Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline application #1

Closed
flowersinthesand opened this issue Mar 20, 2015 · 4 comments
Closed

Offline application #1

flowersinthesand opened this issue Mar 20, 2015 · 4 comments
Assignees
Milestone

Comments

@flowersinthesand
Copy link
Member

See a summary of discussion about this feature

The current design works well with opened socket, online. However, in the real world, it is necessary to consider when connection is closed temporarily, offline. To put it concretely, there must be a way for server or client to send events again which couldn't be sent in time due to temporal disconnection.

The basic idea of this feature is to change the relationship of socket and transport from one-to-one to one-to-many.

Now that transport concept is introduced, an object called socket is not one-time connectivity any more. Therefore, even in server like client, socket reference should be maintained regardless of connection and disconnection and only underlying transport should be replaced internally according to connection and disconnection. For this, socket should have a unique identifier and underlying transports should share that identifier A. Then, when connection is closed temporarily, both server-side and client-side socket can accumulate events which user tries to send to somewhere.

Some events should be resent but some events should be not. For example, assume that live video streaming is done by using binary event. Because it is live, end-user may want to skip frames missed while disconnection. (The server may want to skip missing frames for memory issue as well) In that case, such events should be not cached and resent on reopen. Therefore, application developer should have a flexible way to decide whether or not to cache events that couldn't be sent and send them on socket's reopen B.

Also to tell the permanent close from the temporal close, a kind of timeout is required. It may be not needed in client but cause some memory issue in server. It draws a new state, destroyed, which is an end of life cycle of socket. In other means, if no new transport is established in the timeout e.g. 30s, the corresponding socket regarded as temporally closed should be regarded as permanently closed, C. A temporally closed socket can deal with events which couldn't be sent but a permanently closed socket can't be and shouldn't be used at all any more.

A, B and C are sub tasks of this feature. Any details about sub tasks are not clear and determined yet and should have a more discussion. Especially, more definite definition about socket's life cycle on both side is required. Also terms like open and close and online and offline should be used by definition.

  • A - Extended life cycle of socket.
    • A transport whose URI doesn't have a socket id should be issued a socket id in handshaking the protocol. Then, client-side socket's state transitions from connecting to opened state and server-side socket is constructed.
    • A transport whose URI have a socket id should be injected to the corresponding socket. Then, both client-side and server-side socket's state transitions from closed to opened state.
    • Especially in browser due to page navigation, socket reference can't be maintained. A cookie can be used but is it enough? any side effect?
    • Consider how temporally closed socket and persistently closed socket should reconnect in client.
  • B - How to deal with send method on offline.
    • By introducing new event. This way is used in the below code snippet.
    • By using existing error event with new exception.
    • By throwing a new exception if sending event on offline.
  • C - Temporal close and permanent close.
    • The current closed state corresponds to temporal close and new state and event to determine permanent close is needed.

Let's take a look at new API for this feature briefly. Of course, none of API is determined.

Java Server

// When a socket is constructed,
server.onsocket(socket -> {
    // Here, application can deal with authentication and authorization
    // and tag socket with the result e.g. socket.tag("/account/" + auth.username())

    // A queue containing events the server couldn't send while disconnection
    Queue<Object[]> cache = new ConcurrentLinkedQueue<>();
    // When new transport is injected and socket is reopened,
    socket.onreopen(() -> { // or socket.ontransport?
        // Empties the queue by sending cached event one by one
        // TODO may cause an infinite loop and concurrent issue
        while(!cache.isEmpty()) {
            // Beside args, other metadata related to method like timestamp call may be required
            // So Object[] is not good signature to handle this case
            Object[] args = cache.poll();
            socket.send(args[0], args[1], args[2], args[3]);
        }
    });
    // When an event couldn't be sent since it's offline
    socket.oncouldntsend(args -> {
        // You can filter args to cache or not
        cache.offer(args);
    });

    // ...
    // Now server-side socket has repeated life cycle
    // It should have a method to determine the current state
    socket.state() // [OPENED, CLOSED], DESTROYED

    // After this event, socket can't be and shouldn't be used any more
    socket.ondestroy(() -> {
        // This event is a good place to release other resources 
        // depending on this socket or sharing the life cycle of this socket
        cache.clear();
    });
});

// ...
// Now server's selector methods execute a given action for opened and closed socket
// A socket closed more than 30s are automatically destroyed and evicted from the server
server.all(socket -> {
    // If this socket is in opened, an event will be sent through the underlying connection
    // If this socket is in closed, an event will be passed to couldntsend event as event data
    socket.send("event", data);
});

JavaScript Client

var socket = cettia.open(uri, {
    // Only for browser. This cookie is to share socket's id between page navigation
    // It may cause other confusion and I'm not sure it will work as expected
    cookieName: "cettia"
});

// A queue containing events the client couldn't send while disconnection
var cache = [];
// When a transport establishes a connection,
socket.on("open", () => {
    // Empties the cache by sending cached event one by one
    // TODO may cause an infinite loop
    while(cache.length) {
        // For the same reason with the sever, args should be not Arguments object
        var args = cache.shift();
        socket.send.apply(socket, args);
    }
});
// When an event couldn't be sent since it's offline
socket.on("couldntsend", args => {
    // A chance to determine if args should be resent or not
    cache.push(args);
});

// destroyed state is added but when should socket be in that state in client?
// The current reconnection mechanism might be affected
socket.state() // [connecting, opened, closed], destroyed

// For the same reason with the server
socket.on("destory", () => {
    // Clear the cache
    cache.length = 0;
});

// Now it will work even on offline
// On offline, it will be passed to couldntsend event
socket.send("x", y);
@flowersinthesand
Copy link
Member Author

As for C, differentiation between temporal close and permanent close makes work much harder especially in client. (In server, it is needed to avoid memory leak though) Instead, introducing reset event which is fired when socket id is newly issued is much useful than introducing destroy event in client.

var socket = cettia.open(uri, {
    // Only for browser. This cookie is to share socket's id between page navigation
    // That means events which server couldn't send due to page navigation can be retrieved in the last page
    cookieName: "cettia"
});

// A queue containing events the client couldn't send while disconnection
// TODO not maintained during page navigation
var cache = [];

// When a connection is tried, 
// it replaces waiting event as well waiting state
socket.on("connecting", (delay, attempts) => {
    // delay and attempts are (re)connection delay and (re)connection attempts respectively
});

// When a new socket id is issued, 
// it happens if a socket id used to establish a connection don't exist or is invalid e.g. destroyed socket in the server
// The open event always follows this event
socket.on("reset", () => {
    // Resets objects which have been for the older socket
    cache.length = 0;
});

// When a transport establishes a connection,
socket.on("open", () => {
    // Empties the cache by sending cached event one by one
    while(cache.length) {
        var args = cache.shift();
        socket.send.apply(socket, args);
    }
});

// When an event couldn't be sent since it's offline
socket.on("couldntsend", args => {
    // A chance to determine if args should be resent or not
    cache.push(args);
});

// If the state is not opened, it will be passed to couldntsend event
socket.send("x", y);

@flowersinthesand
Copy link
Member Author

As for B, the best way to cache event when there is no connection temporally in both client and server is to utilize yet another reserved event. Here is the reason.

  • Asynchronous as well as synchronous error handling is possible. While many platform deals with such I/O operations asynchronously, a way for send method to throw an exception or check whether it has active connection can handle only synchronous scenario.
  • Using event handler makes it is possible to centralize code to cache event unlike wrapping send method call in try/catch or if block. (Of course, both ways can be used together)
  • No additional API required. Without event way, method like server.all(actionForSocket) should provide a way to deal with closed sockets as well as opened sockets like server.all(actionForOpenedSocket, actionForClosedSocket).

The code snippet will look like the following.

// cache event is called if socket.state method returns CLOSED
// It's not an error but an expected case
socket.oncache((Object[] args) -> {
    if (/* decides whether to cache args or not*/) {
        cache.add(args);
    }
});

// Or by checking out socket's state manually
if (socket.state() == Socket.State.OPENED) {
    socket.send(...);
} else {
    cache.add(...); // or don't cache it
}

socket.onerror(t -> {
    // This case is distinguished from cache event as it tells some event couldn't be sent although there was an active connection
    // For example, if some message's size is over the limit, it would happen and disconnect the existing connection as well

    // In case of Java, cettia-java-platform doesn't provide normalized exception hierarchy now
    // so that there is no such CouldntSendException and user should use their platform-specific exception
    if (t instanceof CouldntSendException) {
        // This approach is likely to be not possible
        /*
        Object[] args = t.args();
        if (decides whether to cache args or not) {
            cache.add(args);
        }
        */
    }
});

@flowersinthesand
Copy link
Member Author

Summary of discussion about this feature - This feature provides events user can utilize to deal with sockets whose connection is disconnected for a little while properly by making a socket to be backed by multiple transports not just one.

  • A socket can be backed up by multiple transports now.
  • A socket has cache event in addition.
    • cache event - It's called when some event is sent during temporal disconnection.
  • A server-side socket has open event and delete event in addition.
    • server's socket event - It is the corresponding event of client side socket's new event.
    • open event - It's called when a socket is (re)opened.
    • delete event - It's called when a socket is closed for a long time e.g. 10 m and is evicted from the server. This is the end of the life cycle.
  • A client-side socket has reconnect option, new event, connecting event and waiting event in addition.
    • reconnect option - An option to determine whether to connect to the server or not when a connection is disconnected.
    • new event - It's called when the server issues a socket id. This is the beginning of the life cycle and the end of the previous life cycle as well.
    • connecting event - It's called when a transport starts to connect to the server.
    • waiting event - It's called when a transport is closed and a reconnection is scheduled by reconnect option.

Server example

Life cycle

  • server's onsocket -> onopen -> onclose -> onopen
  • server's onsocket -> onopen -> onclose -> after a long time e.g. 10 m -> ondelete
// When a server creates a new socket
server.onsocket(socket -> {
    // Here, application can deal with authentication and authorization

    // A queue containing events the server couldn't send while disconnection
    Queue<Object[]> cache = new ConcurrentLinkedQueue<>();
    // When a new transport is injected and a socket is (re)opened,
    socket.onopen(() -> {
        // Determines the socket's state to avoid infinite loop
        // This state can be one of OPENED, CLOSED and DELETED
        while(socket.state() == State.OPENED && !cache.isEmpty()) {
            // Empties the queue by sending cached event one by one
            Object[] args = cache.poll();
            socket.send((String) args[0], args[1], (Action<?>) args[2], (Action<?>) args[3]);
        }
    });

    // If some event is sent when there is no connection
    socket.oncache(args -> {
        // A chance to determine if args should be resent or not
        cache.offer(args);
    });

    // After this event, socket can't be and shouldn't be used any more
    socket.ondelete(() -> {
        // Here is a good place to release resources having depended the socket
        cache.clear();
    });
});

// Now server's selector methods execute a given action for both opened and closed sockets
// A socket closed for a long time e.g. 10 m are automatically deleted and evicted from the server
server.all(socket -> {
    // If this socket is opened, an event will be sent through the underlying connection
    // If this socket is closed, an event will be passed to cache event as event data
    socket.send("event", data);
});

Client example

Life cycle

  • w/o reconnection
    • onconnecting -> onclose
    • onconnecting -> onnew -> onopen -> onclose
  • w/ reconnection
    • {onconnecting -> onclose -> onwaiting} - repeated
    • onconnecting -> onnew -> onopen -> {onclose -> onwaiting -> onconnecting} - repeated -> onnew - if the network is recovered -> onopen -> ...
var socket = cettia.open(uri, {
    // Only for browser. This option deals with cookie to share socket's id between page navigation
    // That means events which server couldn't send due to page navigation can be retrieved in the last page
    cookieName: "cettia"
});

// A queue containing events the client couldn't send while disconnection
// It might be needed to maintain this queue between page navigation e.g. by using JSON serialization and sessionStorage
var cache = [];

// If a connection is tried, 
socket.on("connecting", () => {});

// When a new socket id is issued, 
// This is the beginning of the life cycle and the end of the previous life cycle
socket.on("new", () => {
    // Resets resources having been used for the older socket
    cache.length = 0;
    // The open event always follows this event but not vice versa
});

// When a transport establishes a connection,
socket.on("open", () => {
    // Determines the socket's state to avoid infinite loop
    while(socket.state() === "opened" && cache.length) {
        // Empties the cache by sending cached event one by one
        var args = cache.shift();
        socket.send.apply(socket, args);
    }
});

// If a reconnection is scheduled after disconnection, 
// delay and attempts are reconnection delay calculated by reconnect option and the total number of reconnection attempts respectively
socket.on("waiting", (delay, attempts) => {});

// If some event is sent when there is no connection
socket.on("cache", args => {
    // A chance to determine if args should be resent or not
    cache.push(args);
});

// If the state is not opened, it will be passed to cache event
socket.send("event", data);

Derivative features

  • Attribute support - A plain map per socket sharing its life cycle with socket e.g. Queue<Object[]> cache = (Queue<Object[]>) socket.attr("cache");
  • Cache support - It provides convenient helper to remove the above boilerplate. e.g. Cacher cacher = new Cacher(); socket.onopen(cacher.openAction()); socket.oncache(cacher.cacheAction());

How to implement is skipped as it is intuitive.

@flowersinthesand
Copy link
Member Author

This feature has landed. Even though multiple commits are involved to this feature, there is no change in the protocol.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant