Skip to content

Deuces, a minimal Redis client based on ♠ Spade, specific for PubSub and Monitor mode.

License

Notifications You must be signed in to change notification settings

rootslab/deuces

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Deuces

CODECLIMATE CODECLIMATE-TEST-COVERAGE

LICENSE GITTIP NPM DOWNLOADS

NPM VERSION TRAVIS CI BUILD BUILD STATUS DEVDEPENDENCY STATUS

NPM GRAPH1

NPM GRAPH2

🂢 Deuces, a minimal Redis client specific for pubsub and monitor mode, then no script cache, no db selection, no transactions support. Code is heavily based on ♠ Spade module, a full-featured Redis client, with a restricted set of features and event types.

Supported commands are:

  • connection: auth, ping, quit
  • pubsub: publish, subscribe, unsubscribe, psubscribe, punsubscribe
  • server: monitor, time

Some features:

  • It implements a simple delayed mechanism for re-connecting to socket when the client connection was not voluntarily interrupted.
  • It collects commands in the queue also when the client is offline.
  • It implements an automatic command rollback mechanism for subscriptions when connection is lost and becames ready again.
  • It implements automatic AUTH password sending on socket (re)connection, configurable via the security constructor option.
  • It correctly handles multiple (p)(un)subscriptions command as we will expect (1 command : multiple replies : multiple callback execution); it was well tested against some weird edge cases. See tests for pubsub.
  • It supports the new PING command signature also in PubSub mode.
  • It implements a polling mechanism, useful to force automatic re-connection when client hangs while in PubSub mode.

🂢 Deuces makes use of some well tested modules:

  • Some commands mix-ins and reply formatters copied from Σ Syllabus module.
  • Sermone to encode commands.
  • Abaco and Bolgia modules to get some utilities.
  • β Bilanx a fast and simplified command queue with rollback mechanism based on ♎ Libra code.
  • Cocker module to properly handle socket reconnection when the connection is lost.
  • Hiboris, a utility module to load hiredis native parser, or to fall back to Boris, a pure js parser module for Redis string protocol; internally Boris uses Peela as command stack.
  • Cucu, a tiny module to handle the scheduled execution of repetitive methods/tasks.
  • Gerry, a tiny module to handle event logging to console, for debugging and testing purpose.

###Table of Contents


###Install

NOTE: only node engines ">=v0.10.x" are supported.

$ npm install deuces [-g]
// clone repo
$ git clone git@github.com:rootslab/deuces.git

install and update devDependencies:

 $ cd Deuces/
 $ npm install --dev
 # update
 $ npm update --dev

require

var Deuces = require( 'deuces' );

See examples.

###Run Tests

$ cd deuces/
$ npm test

NOTE: tests need a running Redis server instance, with default/stock configuration.

###Run Benchmarks

run benchmarks for deuces.

$ cd deuces/
$ npm run bench

NOTE:

  • benchmarks need a running Redis server instance, with default/stock configuration.
  • to switch to the faster hiredis native parser, install devDependencies .

###Constructor

Create an instance, the argument within [ ] is optional.

Deuces( [ Object opt ] )
// or
new Deuces( [ Object opt ] )

####Options

Default options are listed.

opt = {

    /*
     * Hiboris option. For default, the loading
     * of 'hiredis' native parser is disabled
     * in favour of ( the slower ) Boris JS parser.
     */
    , hiredis : false

    /*
     * Cocker socket options
     */
    , socket : {
        path : null
        , address : {
            // 'localhost'
            host : '127.0.0.1'
            , port : 6379
            , family : null
        }
        , reconnection : {
            trials : 3
            , interval : 1000
            /*
             * A value to use for calculating the pause between two
             * connection attempts. Default value is the golden ratio.
             * Final value is calculated as:
             * interval * Math.pow( factor, curr.attempts + 1 )
             */
            , factor : ( Math.sqrt( 5 ) + 1 ) / 2
        }
        , connection : {
            /*
             * encoding could be: 'ascii', 'utf8', 'utf16le' or 
             * 'ucs2','buffer'. It defaults to null or 'buffer'.
             */
            encoding : null
            /*
             * keepAlive defaults to true ( it is false in net.Socket ).
             * Specify a number to set also the initialDelay.
             */
            , keepAlive : true
            /*
             * 'timeout' event delay, default is 0 ( no timeout ).
             */
            , timeout : 0
            /*
            * noDelay is true for default, it disables the Nagle
            * algorithm ( no TCP data buffering for socket.write ).
            */
            , noDelay : true
            /*
             * If true, the socket won't automatically send a FIN
             * packet when the other end of the socket sends a FIN
             * packet. Defaults to false.
             */
            , allowHalfOpen : false
        }
    }
    /*
     * Security options.
     *
     * Options for password sending when the client connects to a particular host.
     * An entry will be automatically added with the socket.address or socket.path
     * defined in the constructor option. However, two sample entries are already
     * present in the cache, holding default values from redis.conf. 
     *
     * Every entry should be a file path ('/path/to/file.sock'), or a network path
     * ('ip:port'), and should contain a:
     *
     * - 'requirepass' property, it contains the Redis password string for the current
     * host. It defaults to null. Whenever a client connection is established and if
     * an entry is found in the security hash. an AUTH command will be sent to Redis,
     * before any other command in the command queue.
     *
     * NOTE: If the AUTH reply is erroneous, an 'authfailed' event will be emitted,
     * then the client will be automatically disconnected to force re-AUTH on
     * reconnection; it also happens if AUTH isn't required by Redis, but was sent
     * by the client.
     * If authorization is granted by Redis, an 'authorize' event will be emitted,
     * then if the command queue is not empty, it will be processed.
     */
     , security : {
        // a network path (ip:port)
        '127.0.0.1:6379' : {
            requirepass : null
        }
        // a unix domain socket path
        , '/tmp/redis.sock' : {
            requirepass : null
        }
    }
    /*
     * Command queue options.
     */
    , queue : {
        /*
         * Set the max size for the rollback queue.
         * It defaults to 2^16, to disable set it to 0.
         */
        rollback : 64 * 1024
        /*
         * Log the last access time to the queue's head.
         * It is disabled for default.
         *
         * NOTE: It is used to store the last time a reply was received. 
         */
        , timestamps : false
    }
}

Back to ToC


###Properties

Don't mess with these properties!

/*
 * A property that holds the initial config object.
 */
Deuces.options : Object

/*
 * An Object that holds all Redis commands/methods mix-ins
 * from Syllabus. It is a shortcut for Deuces.mixins.commands.
 * See https://github.com/rootslab/syllabus#syllabus-commands.
 */
Deuces.commands : Object

/*
 * A flag to indicate if the connection to Redis Server
 * is currently active.
 */
Deuces.ready : Boolean

/*
 * An Object that holds all scheduled tasks.
 * See Deuces#initTasks method to load defaults entries like 'polling'.
 * See Deuces.qq property to handle tasks.
 */
Deuces.tasks : Object

/*
 * Some shortcuts to internal modules.
 */

/*
 * Cocker module that inherits from net.Socket.
 */
Deuces.socket : Cocker

/*
 * Parser module, it could be an instance of Hiboris, a module
 * wrapper for the hiredis native parser, or the Boris JS parser.
 */
Deuces.parser : Hiboris | Boris

/*
 * Libra Queue Manager for Commands/Replies bindings.
 */
Deuces.queue : Bylanx

/*
 * Property that contains utilities and commands
 */
Deuces.mixins : Object

/*
 * Cucu module to handle tasks.
 * See https://github.com/rootslab/cucu
 */
Deuces.qq : Cucu

/*
 * Debug Properties
 */

/*
 * Gerry module to handle events logging.
 * See https://github.com/rootslab/gerry
 */
Deuces.logger : Gerry

Back to ToC


###Methods

Arguments within [ ] are optional.

####connect

Open a connection to the Redis Server:

  • When the connection is fully established, the ready event will be emitted.
  • You can optionally use a callback that will be executed on the ready event.
  • It accepts an optional socket confguration object.
  • It returns the current Deuces instance.

NOTE: You don't need to listen for the ready event, commands will be queued in "offline mode" and written to socket when the connection will be ready.

/*
 * socket_opt = {
 *      address : {
 *          host : '127.0.0.1'
 *          , port : 6379
 *      }
 *      , reconnection : {
 *          trials : 3
 *          , interval : 1000
 *      }
 *      , connection : {
 *          ...
 *      }
 *  }
 */
Deuces#connect( [ Object socket_opt [, Function cback ] ] ) : Deuces

####disconnect

Disconnect client from the Redis Server:

  • You can optionally use a cback that will be executed after socket disconnection.
  • It returns the current Deuces instance.

NOTE: From the client point of view, executing disconnect has the same effect of sending and executing the Redis QUIT command. Connection will be closed and no other re-connection attempts will be made.

Deuces#disconnect( [ Function cback ] ) : Deuces

####initTasks

Load default methods/tasks from 'deuces/lib/tasks' dir, you could restrict files to load, specifying some filenames. It returns the current Deuces.tasks (Cucu.ttable) property.

NOTE: for now, there is only one task, 'polling', contained in the 'connection' file.

Deuces#initTasks() : Cucu

See Cucu to see all available options to handle tasks.


####cli

Enable event logging to console.

This method enables/logs some extra event for debugging/testing purpose:

  • reply for Redis replies.
  • scanqueue when the "offline" command queue is processed.
  • queued for commands executed when the client is offline.

NOTE:

  • the 'enable' option defaults to true.
  • the default 'logger' prints event name and arguments to console.
Deuces#cli( [ Boolean enable [, Function logger [, Boolean collect_events ] ] ] ) : undefined

See "Other Debug Events" section.

Back to ToC


####Redis Commands

The Dueces.commands property contains methods to encode and send Redis commands.

Arguments within [ ] are optional, '|' indicates multiple type for argument.

#####CONNECTION

Redis Connection, 5 commands.

Arguments within [ ] are optional, '|' indicates multiple type for argument.

'auth' : function ( String password [, Function cback ] ) : Object

// legacy ping
'ping' : function ( [ Function cback ] ) : Object

// Redis >= 2.8.x ping, available also in pubsub mode
'ping' : function ( [ String message, [ Function cback ] ] ) : Object

'quit' : function ( [ Function cback ] ) : Object

#####PUBSUB

Redis PubSub, 5 commands.

'publish' : function ( String channel, String message [, Function cback ] ) : Object

'psubscribe' : function ( String pattern | Array patterns [, Function cback ] ) : Object

'punsubscribe' : function ( String pattern | Array patterns [, Function cback ] ) : Object

'subscribe' : function ( Number channel | String channel | Array channels [, Function cback ] ) : Object

'unsubscribe' : function ( [ String channel | Array channels [, Function cback ] ] ) : Object

NOTE: to (p)unsubscribe from all channels/patterns use null or [].

#####SERVER

Redis Server, 2 commands.

'monitor' : function ( [ Function cback ] ) : Object

'time' : function ( [ Function cback ] ) : Object

Back to ToC

####Command Callback

Every command mix-in accepts a callback function as the last argument, it will get 3 arguments:

/*
 * 'is_err_reply' is a Boolean that signals an ERROR reply from Redis,
 * ( not a JS Error ), then reply data will contain the error message(s).
 *
 * 'data' is a list containing reply data Buffers ( or Strings if hiredis is used ).
 *
 * 'reveal' is a utility function that converts the raw Redis Reply in a simple
 * and usable form.
 *
 * NOTE: The utility function is not the same for all command replies, because,
 * as we surely know, some reply needs particular format and type conversions.
 */
'callback' : function ( Boolean is_err_reply, Array data, Function reveal ) : undefined

Example Code:

var log = console.log
    , Deuces = require( 'Deuces' )
    , client = Deuces( {} )
    ;

// start async connection to Redis
client.connect();

// execute TIME command
client.commands.time( function ( is_err_reply, reply_data_arr, reveal_fn ) {
    log( '- error reply:', is_err_reply );
    log( '- raw reply:', reply_data_arr );
    log( '- converted reply:', reveal_fn( reply_data_arr ) );
} );

Back to ToC


###Events

####Events Sequence Diagram

  • the event emitted for first could be: - connect or offline, after the execution of connect or disconnect methods. - error, it "simply" happens.
                             +                              +
                             |                              |
                             v                              v
                          connect+------->(timeout)       error
                             +
                             |
                             +-------->(authorized)
                             |               +
                             V               |
              +         (authfailed)         |
              |              +               |
              v              |               v
  +------->offline<----+-----+------------+ready
  |           +        |                     +
  |           |        |                     |
  +           v        |                     |
lost<----+(*attempt*)  +----+(*monitor*)<----+---->(listen)
  +           +                                       +
  |           |                                       |
  |           |                                       v
  +->connect<-+                     (shutup)<---+(*message*)
        +
        |
        v
       ...

NOTE:

  • events between ( ) could never happen, most of them depends on client configuration.
  • events within * could be emitted more than once, namely 0 or k times with k >= 1.
  • timeout could happen in "any" moment after the connect event.
  • listen signals that client is entering in subscription mode
  • shutup signals that client is leaving subscription mode.
  • monitor mode could end only with a QUIT command (then 'offline').

Back to ToC

####Error Events

/*
 * A parser or command encoding error has occurred.
 */
'error' : function ( Error err, Object command ) : undefined

####Auth Events

These events are emitted on every client (re)connection to Redis and only if AUTH is set to be mandatory for the current connected host; namely, should exist an entry, 'ip:port' or '/path/to/file', in the options.security hash, with requirepass property set to a non empty string.

/*
 * The reply to AUTH command is an Error, then client will be disconnected; it also
 * happens when AUTH is not required by Redis but issued by the client. No 'ready'
 * event could be launched.
 */
'authfailed' : function ( String password, Array reply, Object address ) : undefined

/*
 * Client authorization is successful. After that the command queue will be processed.
 * and the 'ready' event could be launched.
 */
'authorized' : function ( String password, Array reply, Object address ) : undefined

Back to ToC

####Socket Connection Events

/*
 * After that the client has established a connection to the Redis host,
 * it is now ready to write commands to the socket and to receive the
 * relative replies from Redis. It happens after processing AUTH and
 * SELECT commands and finally the offline queue.
 *
 * NOTE: Every commands executed by the client before the 'ready' event,
 * will be enqueued in 'offline mode' and sent/written to socket when
 * 'ready' will be emitted.
 */
'ready' : function ( Object address ) : undefined

/*
 * A client connection is fully established with Redis host. This event
 * happens before 'ready' and AUTH/SELECT related events.
 */
'connect' : function ( Object address ) : undefined

/*
 * Connection is currently down ( on the first 'close' event from the socket ).
 */
'offline' : function ( Object address ) : undefined

/*
 * Client is trying to reconnect to Redis server, k is the number of current
 * connection attempt.
 *
 * NOTE: 'millis' indicates the last interval of time between attempts.
 */
'attempt' : function ( Number k, Number millis, Object address ) : undefined

/*
 * The connection is definitively lost ( after opt.reconnection.trials times ).
 */
'lost' : function ( Object address ) : undefined

/*
 * The socket times out for inactivity.
 * It only notifies that the socket has been idle.
 */
'timeout' : function ( Number timeout,  Object address ) : undefined

Back to ToC

####PubSub Events

NOTE: for multiple (P)(UN)SUBSCRIBE commands, callbacks are executed one time for every message reply those messages will be received also through the Pub/Sub system. However the first callback signals that the command is succesfully processed by Redis.

For example:

  • subscribe( [ 'a', 'a', 'b', 'b', 'c', 'c' ], cback ) :
    • executes callback 6 times
    • produces 6 messages
    • 3 actual subscriptions
  • unsubscribe( null, cback ):
    • executes cback 3 times
    • produces 3 messages
    • 0 subscriptions.
/*
 * A message was received through the PubSub system when the client is in PubSub mode.
 *
 * NOTE: the 'formatter' function converts the received 'message' to an obj/hash.
 * For example, a message reply to a (P)(UN)SUBSCRIBE command issued by the client,
 * could be:
 *
 * 'message' -> [ 'unsubscribe', 'channel', 0 ]
 *
 * will be converted to:
 *
 * {
 *  type : 'unsubscribe'
 *  , chan : 'channel'
 *  . subs : 0
 * }
 *
 * a typical message received from publisher(s):
 *
 * 'message' -> [ 'message', 'channel', 'Hello folks!' ]
 *
 * will be converted to:
 *
 * {
 *  type : 'message'
 *  , chan : 'channel'
 *  . msg : 'Hello folks!!'
 * }
 *
 * See also Syllabus.formatters.
 *
 */
'message' : function ( Array message, Function formatter ) : undefined

/*
 * An event to signal that the client is entering in PubSub mode after a
 * subscription command. From now, all replies to (un)subscription commands
 * will be received as messages.
 */
'listen' : function () : undefined

/*
 * An event to signal that client is leaving PubSub mode after a successfull
 * execution of a unsubscription command.
 * It doesn't happen if the client disconnects while in PubSub mode.
 */
'shutup' : function () : undefined

Back to ToC

####Monitor Events

/*
 * A 'message' was received when the client is in Monitor mode.
 * 
 * NOTE: the 'formatter' function converts the 'message' to an obj/hash.
 * For example, with an input like:
 *
 * message = '1405478664.248185 [0 127.0.0.1:47573] "ping"',
 *
 * executing formatter( message ) will output:
 *
 * {
 *  ip: '127.0.0.1',
 *  port: 47573,
 *  db: 0,
 *  cmd: '"ping"',
 *  utime: 1405478664248,
 *  time: [ 1405478664, 248185 ]
 * }
 *
 * See also Syllabus.formatters.
 *
 */
'monitor' : function ( String message, Function formatter ) : undefined

Back to ToC

####Other Debug Events

NOTE: to enable logging for events below, execute Deuces#cli method.

/*
 * When the client is offline, commands are not sent but queued.
 */
'queued' : function ( Object command, Number offline_queue_size ) : undefined

/*
 * When the client will be online once again, this event is emitted
 * before performing a scan for sending enqueued commands, then always
 * before the 'ready' event.
 */
'scanqueue' : function ( Number offline_queue_size ) : undefined

/*
 * The client receives a command reply from Redis. It always happens after
 * the 'ready' event.
 */
'reply' : function ( Object command, String reply ) : undefined

/*
 * The client receives an error reply from Redis. It always happens after
 * the 'ready' event.
 */
'error-reply' : function ( Object command, String err_reply ) : undefined

Back to ToC


MIT License

Copyright (c) 2014 < Guglielmo Ferri : 44gatti@gmail.com >

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

Deuces, a minimal Redis client based on ♠ Spade, specific for PubSub and Monitor mode.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published