cloudstack-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Ryan Dietrich <r...@betterservers.com>
Subject SocketServer web socket client
Date Fri, 05 Jul 2013 17:34:00 GMT
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);
        }
    }
}






Mime
View raw message