All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nustaq.kontraktor.barebone.RemoteActorConnection Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014 Ruediger Moeller.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.nustaq.kontraktor.barebone;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.nustaq.serialization.FSTConfiguration;
import org.nustaq.serialization.coders.Unknown;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Created by ruedi on 05/06/15.
 *
 * A Json encoded Http Long Poll Client
 *
 * Note all callbacks are delivered in the same single thread.
 *
 * NEVER BLOCK THIS THREAD.
 *
 * Use an ExecutorService to perform blocking code or code with longer duration
 * from within a callback.
 *
 * Objects of unknown (not on classpath) classes returned / required by the Remote Actor are translated
 * into "Unknown" instances. Note that its favourable to create kind of a shared clientprotocol.jar to get
 * properly deserlialized and typed results.
 *
 */
public class RemoteActorConnection {

    public static final int LONG_POLL_MAX_TIME = 15_000;
    public static int MAX_CONN_PER_ROUTE = 1000;
    public static int MAX_CONN_TOTAL = 1000;
    public static boolean DumpProtocol = false;

    protected static CloseableHttpAsyncClient asyncHttpClient;
    protected volatile boolean isConnected;
    private static final boolean SENDDEBUG = false;
    public boolean noSeqChecking = true;

    public CloseableHttpAsyncClient getClient() {
        synchronized (this) {
            if (asyncHttpClient == null ) {
                asyncHttpClient = HttpAsyncClients.custom()
                    .setMaxConnPerRoute(MAX_CONN_PER_ROUTE)
                    .setMaxConnTotal(MAX_CONN_TOTAL)
                    .setDefaultIOReactorConfig(
                        IOReactorConfig.custom()
                            .setIoThreadCount(8)
                            .setSoKeepAlive(true)
                            .setSoReuseAddress(true)
                            .build()
                    ).build();
                asyncHttpClient.start();
            }
            return asyncHttpClient;
        }
    }

    final static Header NO_CACHE = new Header() {
        @Override
        public String getName() {
            return "Cache-Control";
        }
        @Override
        public String getValue() {
            return "no-cache";
        }
        @Override
        public HeaderElement[] getElements() throws ParseException {
            return new HeaderElement[0];
        }
    };

    final static class JWTHeadeer implements Header {
        public JWTHeadeer(String jwt) {
            this.jwt = jwt;
        }

        String jwt;
        @Override
        public String getName() {
            return "JWT";
        }
        @Override
        public String getValue() {
            return jwt;
        }
        @Override
        public HeaderElement[] getElements() throws ParseException {
            return new HeaderElement[0];
        }
    };

    final static class IDHeadeer implements Header {
        public IDHeadeer(String id) {
            this.id = id;
        }

        String id;
        @Override
        public String getName() {
            return "ID";
        }
        @Override
        public String getValue() {
            return id;
        }
        @Override
        public HeaderElement[] getElements() throws ParseException {
            return new HeaderElement[0];
        }
    };

    protected FSTConfiguration conf;
    protected static ExecutorService myExec = Executors.newSingleThreadExecutor();
    protected String sessionId;
    protected String sessionUrl;
    protected int lastSeenSeq;
    protected ConnectionListener connectionListener;
    protected volatile long timeout = LONG_POLL_MAX_TIME*2; // signal close if no longpoll has been received for this time

    protected long lastPing;
    protected String jwt = "";
    protected String id = "";

    /**
     * callback id => promise or callback
     */
    protected ConcurrentHashMap callbackMap = new ConcurrentHashMap<>();
    /**
     * used to generate unique ids for callbacks/promises/actors
     */
    protected AtomicLong idCount = new AtomicLong(0);
    protected boolean requestUnderway = false; // avoid opening a second http connection
    /**
     * buffered requests to be sent, will be batched
     */
    protected ArrayList requests = new ArrayList<>();
    /**
     * number of unreplied 'ask' calls. Backpressure can be done by
     * observing this. Its important your server app always returns a result or error (no silent timeout),
     * else callbacks will stack up and this count will increase. Backpressure by limiting open futures
     * then at some point will stall the client.
     * TODO: add time based timeout for unreplied futures.
     */
    protected AtomicInteger openFutureRequests = new AtomicInteger(0);

    /**
     * create a default configures (json serialization, no shared refs, no pretty print)
     * @param connectionListener
     */
    public RemoteActorConnection(ConnectionListener connectionListener) {
        this(connectionListener,false);
    }

    /**
     * note that shared refs support requires the server to be also configured for json shared refs.
     * as currenlty the js client does not support this, its likely server run with shared refs false.
     *
     * @param sharedRefs
     */
    public RemoteActorConnection(ConnectionListener connectionListener,boolean sharedRefs) {
        initConf(sharedRefs);
        this.connectionListener = connectionListener;
        try {
            if ( Class.forName("org.nustaq.kontraktor.Actor") != null ) {
//                throw new RuntimeException("this client library clashes with full kontraktor release. Use standard kontraktor client if its on the classpath anyway.");
            }
        } catch (ClassNotFoundException e) {
            // expected
        }
        myExec.execute(new Runnable() {
            @Override
            public void run() {
                Thread.currentThread().setName("kontraktor-bare client");
            }
        });
    }

    public ConnectionListener getConnectionListener() {
        return connectionListener;
    }

    protected void initConf(boolean sharedRefs) {
        // support Json encoding only in order to deal with unknown classes
        conf = FSTConfiguration.createJsonConfiguration(DumpProtocol, !DumpProtocol && sharedRefs);
        conf.registerCrossPlatformClassMapping(new String[][]{
            {"call", RemoteCallEntry.class.getName()},
            {"cbw", Callback.class.getName()}
        });
        conf.registerSerializer(Callback.class, new CallbackRefSerializer(this), true);
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public Promise connect(final String url, final boolean longPoll) {
        final Promise res = new Promise();

        byte[] message = conf.asByteArray(null);
        if (DumpProtocol) {
            try {
                System.out.println("auth-req:"+new String(message,"UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        HttpPost req = createRequest(url, message);
        getClient().execute(req, new FutureCallback() {
            @Override
            public void completed(HttpResponse result) {
                lastPing = System.currentTimeMillis();
                if (result.getStatusLine().getStatusCode() != 200) {
                    res.receive(null,"connection failed with status:"+result.getStatusLine().getStatusCode());
                    return;
                }
                String cl = result.getFirstHeader("Content-Length").getValue();
                int len = Integer.parseInt(cl);
                if (len > 0) {
                    final byte resp[] = new byte[len];
                    try {
                        result.getEntity().getContent().read(resp);
                        if (DumpProtocol) {
                            try {
                                System.out.println("auth-resp:" + new String(resp, "UTF-8"));
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                        }
                        myExec.execute(new Runnable() {
                            @Override
                            public void run() {
                                sessionId = (String) conf.asObject(resp);
                                isConnected = true;
                                if ( longPoll ) {
                                    startLongPoll();
                                }
                                sessionUrl = url + "/" + sessionId;
                                System.out.println("session id:" + sessionId);
                                res.complete(new RemoteActor("App", 1, RemoteActorConnection.this), null);
                            }
                        });
                    } catch (Exception e) {
                        res.complete(null, e);
                        if (DumpProtocol)
                            e.printStackTrace();
                    }
                } else {
                    res.complete(null, "connection rejected, no connection id");
                }
            }

            @Override
            public void failed(Exception ex) {
                res.complete(null, ex);
            }

            @Override
            public void cancelled() {
                res.complete(null, "connection failed. Canceled request");
            }
        });

        return res;
    }

    protected void startLongPoll() {
        final AtomicReference lp = new AtomicReference<>();
        lp.set(new Runnable() {
            @Override
            public void run() {
                if ( ! isConnected )
                    return;

                final AtomicInteger timedout = new AtomicInteger(0); // 1 = reply, 2 = timeout
                delayed(new Runnable() {
                    @Override
                    public void run() {
                        checkTimeout();
                        if ( timedout.compareAndSet(0,2) ) {
                            // long poll timeout, retry
                            myExec.execute(lp.get());
                        }
                    }
                }, LONG_POLL_MAX_TIME + 1000 ); // give 1 second trip latency
                HttpPost request = createRequest(sessionUrl, conf.asByteArray(new Object[] { lastSeenSeq } ));
                getClient().execute(request, new FutureCallback() {
                    @Override
                    public void completed(HttpResponse result) {
                        if (!timedout.compareAndSet(0, 1)) {
                            return;
                        }
                        if (result.getStatusLine().getStatusCode() != 200) {
                            log("unexpected return status " + result.getStatusLine().getReasonPhrase());
                            delayed(lp.get(), 2000);
                            return;
                        }
                        lastPing = System.currentTimeMillis();
                        String cl = result.getFirstHeader("Content-Length").getValue();
                        int len = Integer.parseInt(cl);
                        if (len > 0) {
                            final byte b[] = new byte[len];
                            try {
                                result.getEntity().getContent().read(b);
                                myExec.execute(new Runnable() {
                                    @Override
                                    public void run() {
                                        processResponse(b);
                                    }
                                });
                                myExec.execute(lp.get());
                            } catch (Throwable e) {
                                log(e);
                                // delay next longpoll to avoid exception spam
                                delayed(lp.get(), 2000);
                            }
                        } else {
                            myExec.execute(lp.get());
                        }
                    }

                    @Override
                    public void failed(Exception ex) {
                        if (!timedout.compareAndSet(0, 1)) {
                            return;
                        }
                        log(ex);
                        // delay next longpoll to avoid exception spam
                        delayed(lp.get(), 2000);
                    }

                    @Override
                    public void cancelled() {
                        if (!timedout.compareAndSet(0, 1)) {
                            return;
                        }
                        log("request canceled");
                        // delay next longpoll to avoid exception spam
                        delayed(lp.get(), 2000);
                    }
                });
            }
        });
        delayed(lp.get(), 1000);
    }

    protected void checkTimeout() {
        if ( System.currentTimeMillis() - lastPing > timeout )
            disconnect("timed out");
    }

    protected static Timer timer = new Timer();

    /**
     * util to semd delayed jobs to processing/callback thread
     * @param runnable
     * @param millis
     */
    protected void delayed(final Runnable runnable, long millis) {
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    myExec.execute(runnable);
                } catch (Throwable t) {
                    log(t);
                }
            }
        }, millis);
    }

    protected void addRequest(RemoteCallEntry remoteCallEntry, Promise res) {
        if ( res != null ) {
            long key = registerCallback(res);
            remoteCallEntry.futureKey = key;
            openFutureRequests.incrementAndGet();
        }
        synchronized (requests) {
            requests.add(remoteCallEntry);
            sendRequests();
        }
    }

    public int getOpenFutureRequests() {
        return openFutureRequests.get();
    }

    /**
     * send an empty request polling for messages on server side. Unlike long poll,
     * this request returns immediately and is not delayed by the server
     */
    public void sendShortPoll() {
        myExec.execute(new Runnable() {
            @Override
            public void run() {
                Object[] req = new Object[] { "SP", lastSeenSeq };
                sendCallArray(req);
            }
        });
    }

    /**
     * sends pending requests async. needs be executed inside lock (see calls of this)
     */
    protected void sendRequests() {
        if ( ! requestUnderway ) {
            requestUnderway = true;
            delayed( new Runnable() {
                @Override
                public void run() {
                    synchronized (requests) {
                        Object req[];
                        if ( openFutureRequests.get() > 0 && requests.size() == 0 ) {
                            req = new Object[] { "SP", lastSeenSeq };
                        } else {
                            req = new Object[requests.size() + 1];
                            for (int i = 0; i < requests.size(); i++) {
                                req[i] = requests.get(i);
                            }
                            req[req.length - 1] = lastSeenSeq;
                            requests.clear();
                        }

                        sendCallArray(req).then(new Callback() {
                            @Override
                            public void receive(Integer result, Object error) {
                                synchronized (requests ) {
                                    requestUnderway = false;
                                    if ( requests.size() > 0 || (result != null && result > 0 && openFutureRequests.get() > 0 ) ) {
                                        myExec.execute(new Runnable() {
                                            @Override
                                            public void run() {
                                            sendRequests();
                                            }
                                        });
                                    }
                                }
                            }
                        });
                    }
                }
            }, 1);
        }
    }

    /**
     * encodes amd semds (incl seqNo). needs be executed inside lock (see calls of this)
     */
    protected Promise sendCallArray(Object[] req) {
        final Promise p = new Promise<>();
        if (SENDDEBUG)
            System.out.println("SENDING "+(req.length-1));
        for (int i = 0; i < req.length; i++) {
            Object o = req[i];
            if ( o instanceof RemoteCallEntry ) {
                ((RemoteCallEntry) o).pack(conf);
            }
        }
        byte[] message = conf.asByteArray(req);
        HttpPost request = createRequest(sessionUrl, message);
        if ( DumpProtocol ) {
            try {
                System.out.println("req:"+new String(message,"UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        getClient().execute(request, new FutureCallback() {
            @Override
            public void completed(HttpResponse result) {
                if (result.getStatusLine().getStatusCode() != 200) {
                    String error = "Unexpected status:" + result.getStatusLine().getStatusCode();
                    p.reject(error);
                    return;
                }
                lastPing = System.currentTimeMillis();
                String cl = result.getFirstHeader("Content-Length").getValue();
                int len = Integer.parseInt(cl);
                if (len > 0) {
                    final byte b[] = new byte[len];
                    try {
                        result.getEntity().getContent().read(b);
                        myExec.execute(new Runnable() {
                            @Override
                            public void run() {
                                int numMsgResp = 0;
                                try {
                                    numMsgResp = processResponse(b);
                                } finally {
                                    p.complete(numMsgResp,null);
                                }
                            }
                        });
                    } catch (Throwable e) {
                        p.complete(null,e);
                        log(e);
                    }
                } else {
                    p.complete(0,null);
                }
            }

            @Override
            public void failed(Exception ex) {
                p.complete(null,ex);
                log(ex);
            }

            @Override
            public void cancelled() {
                p.complete(0,null);
                log("request canceled");
            }
        });
        return p;
    }

    public String getJwt() {
        return jwt;
    }

    public String getId() {
        return id;
    }

    public RemoteActorConnection jwt(final String jwt) {
        this.jwt = jwt;
        return this;
    }

    public RemoteActorConnection id(final String id) {
        this.id = id;
        return this;
    }


    Map sequenceCache = new HashMap(); // caches early responses in case of response race
    protected int processResponse(byte[] b) {
        if ( DumpProtocol ) {
            try {
                System.out.println("resp:"+new String(b,"UTF-8"));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        Object o[] = (Object[]) conf.asObject(b);
        if ( SENDDEBUG )
            System.out.println("RECEIVE:"+(o.length-1));
        int seq = ((Number) o[o.length - 1]).intValue();
        if ( seq == lastSeenSeq+1 || lastSeenSeq == 0 || noSeqChecking ) {
            lastSeenSeq = seq;
            processDecodedResultArray(o);
            Object next[];
            while ( (next = (Object[]) sequenceCache.get(lastSeenSeq + 1)) != null ) {
                lastSeenSeq++;
                sequenceCache.remove(lastSeenSeq);
                log("replay "+lastSeenSeq);
                processDecodedResultArray(next);
            }
        } else {
            log("ignored result with sequence:" + seq + " lastSeen:" + lastSeenSeq);
            if ( seq > lastSeenSeq ) {
                sequenceCache.put(seq,o);
                if ( sequenceCache.size() > 5 ) {
                    disconnect("Unrecoverable Gap");
                }
            }
        }
        return o.length-1;
    }

    public void close() {
        disconnect("closed");
    }

    protected void disconnect(String s) {
        isConnected = false;
        if ( connectionListener != null ) {
            connectionListener.connectionClosed(s);
        }
        lastSeenSeq = 0;
        myExec.shutdown();
    }

    protected void processDecodedResultArray(Object[] o) {
        for (int i = 0; i < o.length-1; i++) {
            final RemoteCallEntry call = (RemoteCallEntry) o[i];
            if ( call.getQueue() == 1 ) // callback
            {
                call.unpack(conf);
                transformRecursive(conf,  call.getArgs(),null, -1, null);
                // catch and transform direct remote actor reference
                if ( call.getArgs()[0] instanceof Unknown) {
                    Unknown uk = (Unknown) call.getArgs()[0];
                    List items = uk.getItems();
                    if ( items.size() == 2 ) {
                        if ( items.get(0) instanceof Number &&
                             items.get(1) instanceof String &&
                             ((String) items.get(1)).endsWith("_ActorProxy")
                           ) {
                            // actor proxy detected
                            String actorName = (String) items.get(1);
                            actorName = actorName.substring(0,actorName.length()-"_ActorProxy".length());
                            call.getArgs()[0] = new RemoteActor(
                                actorName,
                                ((Number) items.get(0)).intValue(),
                                RemoteActorConnection.this
                            );
                        }
                    }
                }
                // process future callback or callback
                final Callback callback = callbackMap.get(call.getReceiverKey());
                if ( callback == null ) {
                    log("unknown callback receiver "+call);
                } else {
                    final Object error = call.getArgs()[1];
                    if ( callback instanceof Promise) {
                        openFutureRequests.decrementAndGet();
                        callbackMap.remove(call.getReceiverKey());
                    }
                    else {
                        if ( ! call.isContinue() ) {
                            callbackMap.remove(call.getReceiverKey());
                        }
                    }
                    myExec.execute(new Runnable() {
                        @Override
                        public void run() {
                            callback.receive(call.getArgs()[0], error);
                        }
                    });
                }
            }
        }
    }

    protected void transformRecursive(FSTConfiguration conf, Object obj, Object parent, int parindex, String attr) {
        if ( obj instanceof Object[] ) {
            Object arr[] = (Object[]) obj;
            for (int i = 0; i < arr.length; i++) {
                Object o = arr[i];
                transformRecursive(conf, o, (Object[]) obj, i,null);
            }
        } else if ( obj instanceof Unknown ) {
            Unknown unk = (Unknown) obj;
            // actor proxy detected
            String actorName = unk.getType();
            if ( actorName != null && unk.getType().endsWith("_ActorProxy") ) {
                actorName = actorName.substring(0, actorName.length() - "_ActorProxy".length());
                RemoteActor replaced = new RemoteActor(
                    actorName,
                    ((Number) unk.getItems().get(0)).intValue(),
                    RemoteActorConnection.this
                );
                if ( parent instanceof Object[] && parindex >= 0 ) {
                    ((Object[])parent)[parindex] = replaced;
                } else if ( parent instanceof Unknown ) {
                    if ( parindex >= 0 ) {
                        ((Unknown)parent).getItems().set(parindex,replaced);
                    } else {
                        ((Unknown)parent).set(attr,replaced);
                    }
                }
            } else {
                if ( unk.isSequence() ) {
                    for (int j = 0; j < unk.getItems().size(); j++) {
                        Object obj1 = unk.getItems().get(j);
                        transformRecursive(conf,obj1,unk,j,null);
                    }
                } else {
                    Map fields = unk.getFields();
                    for (String s : fields.keySet()) {
                        transformRecursive(conf,unk.get(s),unk,-1,s);
                    }
                }
            }
        }
    }

    protected long registerCallback(Callback res) {
        long key = idCount.incrementAndGet();
        callbackMap.put(key, res );
        return key;
    }

    protected void log(Throwable e) {
        e.printStackTrace();
    }

    protected void log(String s) {
        System.out.println(s);
    }

    protected HttpPost createRequest(String url, byte[] message) {
        HttpPost req = new HttpPost(url);
        req.addHeader(NO_CACHE);
        req.addHeader(new JWTHeadeer(jwt));
        req.addHeader(new IDHeadeer(id));
        req.setEntity(new ByteArrayEntity(message));
        return req;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy