You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@cloudstack.apache.org by Ryan Dietrich <ry...@betterservers.com> on 2013/07/05 19:34:00 UTC

SocketServer web socket client

Hi, this email's purpose is to let you know on the progress made toward transforming our application into an asynchronous event engine.  At this point, large swaths of progress have been made in cloudstack as well as our socket server technology, but eventually the UI has to start consuming this new functionality, so I have started going further up the chain to detail how these new services can be used with our existing management pages.

* Cloudstack now publishes async job events to rabbitMQ event bus
* AsyncDaemon (an engine I wrote and is in gitlab) can listen to RabbitMQ messages and publish them to our Netty based socket servers
* AsyncDaemon can listen for BSAPI commands, make the async request to BSAPI and return that data to the requestor.
* The WebSocketClient (listed far below) can consume the outputs of everything listed above (plus widgets) through a callback driven interface

So, I have had BSAPI calls going through the socket server (the one used for widgets currently) for some time now.  My admin page had a rudimentary interface for getting calls and callbacks.  This was not viable for use with the portal however.

I've gone ahead and written a proper client.  It's not perfect, there are a few places where I need to refactor, but I typically don't do that kind of stuff until I fully understand the problems with the code.  I am using it in the "new" admin page, that I have committed but not pushed to dev yet.  But I thought it would be better to get it out in the open now before too much time goes by.

If you don't already know, I am a huge proponent of callback driven systems.  I really dig closures, and feel that it is javascript's strongest tool for dealing with asynchronicity, so I use it quite liberally in this module.  The basic idea is:  You can provide callbacks for just about anything, events, widgets, BSAPI commands, or just have generic handlers that can handle everything.  I take care of lining up asynchronous responses from BSAPI back to the caller for you, no pre-registration of events and UUID's like the current implementation.  Events, for BSAPI jobs you submitted would be routed to the appropriate callback as well.  Events that you did not request would ALSO be routed to an appropriate callback, though it would be a generic handler, as these are ad-hoc events that you need to "deal with", rather than something you requested and are expecting.

Going forward, I feel that management page developers should not even have to touch this module at all.  They should be dealing with "lifecycle" actions, which are composed of actions, asynchronous updates, and completions, but that is for another day.  Below is the code, and attached is the vision for UI architecture in an event driven world.


/*
    WebSocketClient

    This module is responsible for the following

    1. Establish connection
    2. Define handling methods for the socket
    3. Create methods for adding subscriptions for callbacks
    4. Create auto-reconnect logic (not done yet)
    5. Create BSAPI command send method, and provide a callback when that method completes
    6. Create widget subscribe/unsubscribe methods
    7. Create structures for handling event callbacks and routing those events to those callbacks
*/

// Helper functions for UUID generation
function s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
        .toString(16)
        .substring(1);
}

function uuid() {
    return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

// ===============================

function WebSocketClient(host, path) {
    var socket;
    this.host = host;
    this.path = path; // verify path starts with a slash
    this.connectTime = 0;

    var myThis = this;
    function myResp(message) {
        return myThis.handleBSAPIResponse(message);
    }

    this.state = {
        'bsapiResponse'  : {   // gets called for all bsapi responses
            'defaultBSAPIResponse' : myResp
        },
        'bsapiCallbacks' : {}, // holds user defined callbacks for BSAPI commands
        'widget'         : {}, // gets called when we receive a widget
        'event'          : {}, // gets called when we receive an event
        'onOpen'         : {}, // gets called when we receive an onOpen event from the websocket
        'onClose'        : {}, // gets called when we receive an onClose event from the websocket
        'default'        : {}  // gets called when we have nothing else to handle things (a catchall)
    };
}

// Connect to our host / path and set up our basic handlers
WebSocketClient.prototype.connect = function() {
    var url = "wss://" + this.host + this.path;
    try {
        if ( 'WebSocket' in window ) {
            socket = new WebSocket(url);
        } else if ( 'MozWebSocket' in window ) {
            socket = new MozWebSocket(url);
        }
    } catch(ex) {
        console.log("Error creating websocket, url=" + url + ", error=", ex);
        return false;
    }

    if ( ! socket ) {
        throw "Unable to create websocket, fall back to ajax";
    }

    var wsc = this;
    socket.onmessage = function(evt) { wsc.webSocketOnMessage(evt); };
    socket.onopen    = function(evt) { wsc.webSocketOnOpen(evt);    };
    socket.onclose   = function(evt) { wsc.webSocketOnClose(evt);   };
    socket.onerror   = function(evt) { wsc.webSocketOnError(evt);   };

    return true;
}

// Socket state commands
WebSocketClient.prototype.isConnected = function() {
    return ( socket.readyState == socket.OPEN ? true : false );
}

WebSocketClient.prototype.getBufferedAmount = function() {
    return socket.bufferedAmount;
}

WebSocketClient.prototype.send = function(msg) {
    if ( this.isConnected() ) {
        socket.send(msg + "\n");
    } else {
        // XXX call callback for failed attempt
    }
}

// Start websocket commands
WebSocketClient.prototype.webSocketOnMessage = function(evt) {
    var msg = JSON.parse(evt.data);
    if ( msg['command'] ) {
        var cmd = msg['command'];
        if ( this.state[cmd] && Object.keys(this.state[cmd]).length > 0 ) {
            for ( var callbackId in this.state[cmd] ) {
                this.state[cmd][callbackId](msg);
            }
            return;
        }
    }
    // call the default handler for the message
    for ( var callbackId in this.state['default'] ) {
        this.state['default'][callbackId](msg);
    }
}

WebSocketClient.prototype.webSocketOnOpen = function(evt) {
    this.connectTime = new Date();
    var msg = evt.data;

    var cmd = "onOpen";
    if ( this.state[cmd] && Object.keys(this.state[cmd]).length > 0 ) { // XXX move to function when refactor
        for ( var callbackId in this.state[cmd] ) {
            this.state[cmd][callbackId](msg);
        }
        return;
    }
}

WebSocketClient.prototype.webSocketOnClose = function(evt) {
    // XXX re-connect logic, it's Wes time!
    console.log("on close: " + evt);
}

WebSocketClient.prototype.webSocketOnError = function(evt) {
    console.log("on error: " + evt);
    // XXX hmm.. need to think of some clever handling bits for this one
}
// End web socket commands

// Application level commands
WebSocketClient.prototype.sendBSAPI = function(cmd, callback) {
    cmd['command']    = 'bsapiCommand';
    cmd['identifier'] = uuid();

    if ( callback ) {
        this.addBSAPICallback(cmd['identifier'], callback);
    }

    this.send(JSON.stringify(outbound));
    return cmd['identifier'];
}

WebSocketClient.prototype.sendCommand = function(command, callback) {
    if ( this.isConnected() ) {
        this.send(command);
        return true;
    } else {
        return false;
    }
}

WebSocketClient.prototype.widgetSubscribe = function(instance) {
    var subscribeString = JSON.stringify({ "command" : "subscribe", "instance" : instance });
    this.send(subscribeString);
}

WebSocketClient.prototype.widgetUnsubscribe = function(instance) {
    var unsubscribeString = JSON.stringify({ "command" : "unsubscribe", "instance" : instance });
    this.send(unsubscribeString);
}

WebSocketClient.prototype.addCallback = function(command, callbackId, callback) {
    if ( this.state[command] ) {
        if ( this.state[command][callbackId] ) {
            throw "callback already defined for command=" + command + ", callbackId=" + callbackId;
        } else {
            this.state[command][callbackId] = callback;
        }
    } else {
        throw "Invalid callback command: " + command;
    }
}

WebSocketClient.prototype.addBSAPICallback = function(callbackId, callback) {
    this.state['bsapiCallbacks'][callbackId] = callback;
}

WebSocketClient.prototype.handleBSAPIResponse = function(message) {
    cbid = message['identifier'];
    if ( this.state['bsapiCallbacks'][cbid] ) {
        this.state['bsapiCallbacks'][cbid](message);
    } else {
        for ( var callbackId in this.state['default'] ) {
            this.state['default'][callbackId](message);
        }
    }
}