├── .gitignore ├── .travis.yml ├── MIT-LICENSE.txt ├── README.md ├── libsparsehash.pc ├── package.json ├── redis-sync.js └── tests ├── hashCommands.txt ├── keyCommands.txt ├── listCommands.txt ├── redisTest ├── setCommands.txt ├── stringCommands.txt ├── test.js └── zsetCommands.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#*\# 3 | node_modules 4 | dump.rdb 5 | tests/test.out 6 | tests/test2.out 7 | tests/redis.out 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | before_install: 6 | - sudo apt-get update 7 | - sudo apt-get install libsparsehash-dev redis-server 8 | - sudo cp libsparsehash.pc /usr/lib/pkgconfig/ 9 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Carlos Guerreiro, http://perceptiveconstructs.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redis-sync 2 | ========== 3 | 4 | A [node.js](http://nodejs.org/) [redis](http://redis.io) [replication](http://redis.io/topics/replication) slave toolkit. 5 | 6 | `redis-sync` implements the replication slave side of the `SYNC` command, streaming in all commands that modify the dataset. 7 | It can also use [rdb-parser](https://github.com/pconstr/rdb-parser) to parse the dataset dump that precedes the commands. 8 | If it can't connect, gets disconnected or if redis is still loading the dataset, `redis-sync` will keep trying to reconnect - with exponential backoff. 9 | 10 | Installation 11 | ------------ 12 | 13 | `npm install redis-sync` 14 | 15 | Usage 16 | ----- 17 | 18 | ```javascript 19 | var redisSync = require('redis-sync'); 20 | var sync = new redisSync.Sync(); 21 | 22 | sync.on('command', function(command, args) { 23 | console.log('command', command, args); 24 | }); 25 | 26 | sync.on('inlineCommand', function(buffers) { 27 | // the server sends regular PING commands 28 | console.log('inline command', buffers); 29 | }); 30 | 31 | sync.on('error', function(err) { 32 | // listen to 'error' and rely on reconnection logic - otherwise it will get thrown 33 | console.error(err); 34 | }); 35 | 36 | sync.connect(); 37 | ``` 38 | 39 | Upon connection, the master will transfer the entire database in RDB format, before sending any commands. 40 | `redis-sync` can use [rdb-parser](https://github.com/pconstr/rdb-parser) to parse it as it streams in. 41 | 42 | You can listen to `entity` events on the `sync` object: 43 | 44 | ```javascript 45 | sync.on('entity', function(e) { 46 | console.log(e); 47 | }); 48 | ``` 49 | 50 | or obtain the entity stream as it starts coming in 51 | 52 | ```javascript 53 | sync.on('rdb', function(rdb) { 54 | rdb.on('entity', function(e) { 55 | console.log(e); 56 | }); 57 | rdb.on('error', function(err) { 58 | // listen to 'error' and rely on reconnection logic - otherwise it will get thrown 59 | console.error(err); 60 | }); 61 | rdb.on('end', function() { 62 | console.log('end of rdb'); 63 | }); 64 | ``` 65 | 66 | Note that in case of reconnection redis will send the database again, emitting 'rdb' and 'entity' events again on the same `sync` object. 67 | 68 | License 69 | ------- 70 | 71 | (The MIT License) 72 | 73 | Copyright (c) 2011-2012 Carlos Guerreiro, [perceptiveconstructs.com](http://perceptiveconstructs.com) 74 | 75 | Permission is hereby granted, free of charge, to any person obtaining 76 | a copy of this software and associated documentation files (the 77 | "Software"), to deal in the Software without restriction, including 78 | without limitation the rights to use, copy, modify, merge, publish, 79 | distribute, sublicense, and/or sell copies of the Software, and to 80 | permit persons to whom the Software is furnished to do so, subject to 81 | the following conditions: 82 | 83 | The above copyright notice and this permission notice shall be 84 | included in all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 89 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 90 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 91 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 92 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 93 | -------------------------------------------------------------------------------- /libsparsehash.pc: -------------------------------------------------------------------------------- 1 | prefix=/usr 2 | exec_prefix=${prefix} 3 | libdir=${exec_prefix}/lib 4 | includedir=${prefix}/include 5 | 6 | Name: sparsehash 7 | Version: 1.10 8 | Description: hash_map and hash_set classes with minimal space overhead 9 | URL: http://code.google.com/p/google-sparsehash 10 | Requires: 11 | Libs: 12 | Cflags: -I${includedir} 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-sync", 3 | "version": "0.1.4", 4 | "keywords": ["redis", "slave", "replication", "sync"], 5 | "homepage": "https://github.com/pconstr/redis-sync", 6 | "description": "redis replication slave", 7 | "main": "redis-sync.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pconstr/redis-sync" 11 | }, 12 | "engines": { 13 | "node": ">=0.4.0 <0.9.0" 14 | }, 15 | "author": "Carlos Guerreiro 0 || that.listeners('rdb').length > 0) && !readRDB) { 108 | if(!rdbParser) { 109 | rdbParser = new rdb.Parser(); 110 | rdbParser.on('entity', function(e) { 111 | that.emit('entity', e); 112 | }); 113 | if(that.listeners('rdb').length === 0) { 114 | rdbParser.on('error', function(err) { 115 | // stream is used internally, error handling is done at the outer level 116 | }); 117 | } 118 | } 119 | that.emit('rdb', rdbParser); 120 | startReadingBytes(bulkReplyLen, false, 121 | function(buf) { rdbParser.write(buf); }, 122 | function() { rdbParser.end(); readRDB = true; rdbParser = undefined; connectedOK(); state = 'ready';}); 123 | } else { 124 | startReadingBytes(bulkReplyLen, false, function(buf) { that.emit('bulkReplyData', buf); } , function() { that.emit('bulkReplyEnd'); readRDB = true; connectedOK(); state = 'ready';}); 125 | } 126 | } 127 | break; 128 | case 'readBytes': 129 | var bytesEnd = bytesStart + bytesLen; 130 | var completed = false; 131 | if(bytesEnd > data.length) { 132 | bytesEnd = data.length; 133 | } else { 134 | completed = true; 135 | } 136 | bytesCBData(data.slice(bytesStart, bytesEnd)); 137 | bytesLen = bytesLen - (bytesEnd - bytesStart); 138 | i = bytesEnd; 139 | if (completed) { 140 | if(bytesExpectingTail) { 141 | state = 'bytesTail'; 142 | } else { 143 | bytesCBEnd(); 144 | } 145 | } else { 146 | bytesStart = 0; 147 | } 148 | break; 149 | case 'bytesTail': 150 | if(data[i] === 13) { // \r 151 | ++i; 152 | state = 'bytesTailR'; 153 | } else { 154 | throw 'parsing error: expected CR after bytes'; 155 | } 156 | break; 157 | case 'bytesTailR': 158 | if(data[i] === 10) { // \n 159 | ++i; 160 | bytesCBEnd(); 161 | } else { 162 | throw 'parsing error: expecting LF after CR after bytes'; 163 | } 164 | break; 165 | case 'unified': 166 | unifiedNArg = parseNumToCr(unifiedNArg); 167 | if(i !== data.length) { 168 | ++i; 169 | state = 'unifiedR'; 170 | } 171 | break; 172 | case 'unifiedR': 173 | if(data[i] === 10) { // \n 174 | ++i; 175 | if(unifiedNArg > 0) { 176 | state = 'unifiedArg'; 177 | unifiedArgs = []; 178 | } else { 179 | state = 'ready'; 180 | } 181 | } else { 182 | throw 'parsing error: expected LF after CR after number of arguments'; 183 | } 184 | break; 185 | case 'unifiedArg': 186 | if(data[i] === 36) { // $ 187 | ++i; 188 | unifiedArgLen = 0; 189 | state = 'unifiedArgLen'; 190 | } else { 191 | throw('parsing error: expected $ at start of argument'); 192 | } 193 | break; 194 | case 'unifiedArgLen': 195 | unifiedArgLen = parseNumToCr(unifiedArgLen); 196 | if(i !== data.length) { 197 | ++i; 198 | state = 'unifiedArgLenR'; 199 | } 200 | break; 201 | case 'unifiedArgLenR': 202 | if(data[i] === 10) { // \n 203 | ++i; 204 | unifiedArg = []; 205 | startReadingBytes(unifiedArgLen, true, function(buf) { 206 | unifiedArg.push(buf); 207 | }, function() { 208 | unifiedArgs.push(unifiedArg); 209 | --unifiedNArg; 210 | if(unifiedNArg > 0) { 211 | state = 'unifiedArg'; 212 | } else { 213 | if(unifiedArgs.length > 0) { 214 | var command = Buffer.concat(unifiedArgs[0]).toString('ascii').toLowerCase(); 215 | that.emit('command', command, unifiedArgs.slice(1)); 216 | } 217 | state = 'ready'; 218 | } 219 | }); 220 | } else { 221 | throw 'parsing error: expected LF after CR at the end of unified arg len'; 222 | } 223 | break; 224 | case 'inline': 225 | skipToCR(); 226 | if(i != inlineCommandStart) 227 | inlineCommandBuffers.push(data.slice(inlineCommandStart, i)); 228 | if(i === data.length) { 229 | inlineCommandStart = 0; 230 | } else { 231 | ++i; 232 | state = 'inlineR'; 233 | } 234 | break; 235 | case 'inlineR': 236 | if(data[i] === 10) { // \n 237 | // check 1st char for error 238 | state = 'ready'; ++i; 239 | if(inlineCommandBuffers.length > 0 && inlineCommandBuffers[0][0] === '-'.charCodeAt(0)) { 240 | // retry sync after a while 241 | error(new Error(Buffer.concat(inlineCommandBuffers).toString())); 242 | reconnect(); 243 | } else { 244 | that.emit('inlineCommand', inlineCommandBuffers); 245 | } 246 | } else { 247 | throw 'parsing error: expected LF after CR at the end of inline command'; 248 | } 249 | break; 250 | default: 251 | throw 'parsing error: unknown state'; 252 | } 253 | } 254 | 255 | function parse(d) { 256 | data = d; 257 | i = 0; 258 | while(i < data.length) { 259 | parseBuffer(); 260 | } 261 | } 262 | 263 | function tryConnect() { 264 | var connId = Math.random(); 265 | state = 'ready'; 266 | readRDB = false; 267 | rdbParser = undefined; 268 | client = net.connect(port, host); 269 | client.on('connect', function(a) { 270 | client.write('sync\r\n'); 271 | }); 272 | 273 | client.on('data', function(data) { 274 | parse(data); 275 | }); 276 | client.on('error', function(err) { 277 | error(err); 278 | reconnect(); 279 | }); 280 | client.on('end', function() { 281 | reconnect(); 282 | }); 283 | } 284 | 285 | function reconnect() { 286 | if(client) { 287 | client.removeAllListeners(); 288 | client.destroy(); 289 | client = undefined; 290 | } 291 | setTimeout(tryConnect, retryDelay); 292 | retryDelay = retryDelay * retryBackoff; 293 | } 294 | 295 | function connectedOK() { 296 | retryDelay = initialRetryDelay; 297 | } 298 | 299 | that.connect = function(p, h) { 300 | port = p; host = h; 301 | retryDelay = initialRetryDelay; 302 | if (port === undefined) { 303 | port = 6379; 304 | } 305 | tryConnect(); 306 | }; 307 | } 308 | 309 | util.inherits(Sync, EventEmitter); 310 | 311 | exports.Sync = Sync; 312 | exports.types = rdb.types; 313 | -------------------------------------------------------------------------------- /tests/hashCommands.txt: -------------------------------------------------------------------------------- 1 | hset h1 a "aha" 2 | hset h1 b "a bit longer, but not very much" 3 | hset h1 c "now this is quite a bit longer, but sort of boring...................................................................................................................................................................................................................................................................................................................................................................." 4 | 5 | hset h2 a 10 6 | hincrby h2 a 1000 7 | hincrby h2 a 100000 8 | 9 | hset h3 a a 10 | hset h3 b b 11 | hdel h3 a 12 | hmset h3 b b2 c c2 13 | hsetnx h3 d d 14 | -------------------------------------------------------------------------------- /tests/keyCommands.txt: -------------------------------------------------------------------------------- 1 | set k1 ssssssss 2 | set k2 wwwwwwww 3 | rename k2 k3 4 | -------------------------------------------------------------------------------- /tests/listCommands.txt: -------------------------------------------------------------------------------- 1 | lpush l1 "aha" 2 | lpush l1 "yup" 3 | 4 | rpush l2 "something" 5 | rpush l2 "now a bit longer and perhaps more interesting" 6 | 7 | rpush l3 "this one is going to be longer -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------" 8 | rpush l3 "a bit more" 9 | 10 | rpush l4 a 11 | rpush l4 b 12 | rpush l4 c 13 | rpush l4 d 14 | rpush l4 e 15 | 16 | lpop l4 17 | rpop l4 18 | 19 | 20 | rpush l5 a 21 | rpushx l5 b 22 | lpushx l5 c 23 | lrem l5 0 b 24 | 25 | rpush l6 a 26 | rpush l6 c 27 | linsert l6 AFTER a b 28 | ltrim l6 1 1 29 | 30 | rpush l7 a 31 | rpush l7 b 32 | rpush l7 c 33 | 34 | brpoplpush l7 l8 10 35 | 36 | rpush l8 1 37 | rpush l8 2 38 | rpush l8 3 39 | rpush l8 4 40 | 41 | rpush l9 10001 42 | rpush l9 10002 43 | rpush l9 10003 44 | rpush l9 10004 45 | 46 | rpush l10 100001 47 | rpush l10 100002 48 | rpush l10 100003 49 | rpush l10 100004 50 | 51 | rpush l11 9999999999 52 | rpush l11 9999999998 53 | rpush l11 9999999997 54 | 55 | sort l11 STORE l12 56 | -------------------------------------------------------------------------------- /tests/redisTest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # kill any leftover redis server 4 | killall redis-server >/dev/null 2>&1 5 | sleep 1 6 | rm -f dump.rdb 7 | 8 | # start redis server 9 | redis-server >./tests/redis.out& 10 | 11 | sleep 3 12 | 13 | # fill in with data 14 | redis-cli -x <./tests/stringCommands.txt >/dev/null 15 | redis-cli -x <./tests/hashCommands.txt >/dev/null 16 | redis-cli -x <./tests/listCommands.txt >/dev/null 17 | redis-cli -x <./tests/setCommands.txt >/dev/null 18 | redis-cli -x <./tests/zsetCommands.txt >/dev/null 19 | redis-cli -x <./tests/keyCommands.txt >/dev/null 20 | 21 | # run test 22 | ./tests/test.js 23 | 24 | killall redis-server 25 | -------------------------------------------------------------------------------- /tests/setCommands.txt: -------------------------------------------------------------------------------- 1 | sadd set1 a b c d 2 | 3 | sadd set2 a b c d 4 | smove set2 set3 b 5 | 6 | srem set2 c 7 | 8 | sadd set4 1 2 3 4 5 6 7 8 9 10 9 | 10 | sadd set5 100000 100001 100002 100003 11 | 12 | sadd set6 9999999999 9999999998 9999999997 13 | -------------------------------------------------------------------------------- /tests/stringCommands.txt: -------------------------------------------------------------------------------- 1 | append s1 "" 2 | append s1 "." 3 | append s1 "aha" 4 | append s1 "a bit longer and with spaces" 5 | append s1 "longer than 256 characters and trivially compressible --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------" 6 | 7 | decr n1 8 | decrby n1 5 9 | 10 | incr n2 11 | incrby n2 500 12 | 13 | incr n3 14 | incrby n3 500000 15 | 16 | mset n4 1 n5 1000 n6 1000000 17 | 18 | msetnx n4b 1 n5b 1000 n6b 1000000 19 | 20 | setbit b1 0 1 21 | setbit b1 1 1 22 | setbit b1 2 1 23 | setbit b1 3 1 24 | setbit b1 4 1 25 | setbit b1 5 1 26 | setbit b1 6 1 27 | setbit b1 7 1 28 | 29 | setbit b2 8 1 30 | setbit b2 9 1 31 | setbit b2 10 1 32 | setbit b2 11 1 33 | setbit b2 12 1 34 | setbit b2 13 1 35 | setbit b2 14 1 36 | setbit b2 15 1 37 | 38 | setbit b3 16 1 39 | setbit b3 17 1 40 | setbit b3 18 1 41 | setbit b3 19 1 42 | setbit b3 20 1 43 | setbit b3 21 1 44 | setbit b3 22 1 45 | setbit b3 23 1 46 | 47 | setbit b4 24 1 48 | setbit b4 25 1 49 | setbit b4 26 1 50 | setbit b4 27 1 51 | setbit b4 28 1 52 | setbit b4 29 1 53 | setbit b4 30 1 54 | setbit b4 31 1 55 | 56 | setbit b5 32 1 57 | setbit b5 33 1 58 | setbit b5 34 1 59 | setbit b5 35 1 60 | setbit b5 36 1 61 | setbit b5 37 1 62 | setbit b5 38 1 63 | setbit b5 39 1 64 | 65 | setnx s2 "now exists" 66 | 67 | setrange s2 3 "_" 68 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*jslint white: true, browser: true, plusplus: true, vars: true, nomen: true, bitwise: true*/ 4 | 5 | /* Copyright 2011 Carlos Guerreiro 6 | All rights reserved */ 7 | 8 | 'use strict'; 9 | 10 | var assert = require('assert'); 11 | var exec = require('child_process').exec; 12 | var rh = require('rawhash'); 13 | var rs = require('../redis-sync.js'); 14 | 15 | var receivedEntities = {}; 16 | var receivedEntities2 = {}; 17 | var commandCounts = {}; 18 | 19 | var expectedCommandCounts = { 20 | append: 5, 21 | decr: 1, 22 | decrby: 1, 23 | incr: 2, 24 | incrby: 2, 25 | mset: 1, 26 | setbit: 40, 27 | setrange: 1, 28 | hset: 6, 29 | hincrby: 2, 30 | hdel: 1, 31 | hmset: 1, 32 | lpush: 2, 33 | rpush: 30, 34 | lpop: 1, 35 | rpop: 1, 36 | rpushx: 1, 37 | lpushx: 1, 38 | lrem: 1, 39 | linsert: 1, 40 | ltrim: 1, 41 | rpoplpush: 1, 42 | sort: 1, 43 | sadd: 1, 44 | smove: 1, 45 | srem: 1, 46 | zadd: 1, 47 | zincrby: 1, 48 | zrem: 1, 49 | set: 2, 50 | rename: 1 }; 51 | 52 | var sync = new rs.Sync(); 53 | 54 | function storeEntity(r, e) { 55 | var h = r[e[0]]; 56 | if(!h) { 57 | r[e[0]] = h = new rh.Dense(); 58 | } 59 | h.set(e[1], e[2]); 60 | } 61 | 62 | sync.on('entity', function(e) { 63 | storeEntity(receivedEntities, e); 64 | }); 65 | 66 | sync.on('rdb', function(rdb) { 67 | rdb.on('entity', function(e) { 68 | storeEntity(receivedEntities2, e); 69 | }); 70 | rdb.on('error', function(err) { 71 | console.error(err); 72 | }); 73 | rdb.on('end', function() { 74 | }); 75 | }); 76 | 77 | sync.on('command', function(command, args) { 78 | commandCounts[command] = 1 + (commandCounts[command] || 0); 79 | }); 80 | 81 | function keyCount(rh) { 82 | var count = 0; 83 | rh.each(function(k, v) { ++count; }); 84 | return count; 85 | } 86 | 87 | var pingCount = 0; 88 | 89 | sync.on('inlineCommand', function(buffers) { 90 | ++pingCount; 91 | if(pingCount === 1) { 92 | assert(keyCount(receivedEntities[rs.types.REDIS_STRING]) === 18); 93 | assert(keyCount(receivedEntities2[rs.types.REDIS_STRING]) === 18); 94 | assert(keyCount(receivedEntities[rs.types.REDIS_LIST]) === 12); 95 | assert(keyCount(receivedEntities2[rs.types.REDIS_LIST]) === 12); 96 | assert(keyCount(receivedEntities[rs.types.REDIS_SET]) === 6); 97 | assert(keyCount(receivedEntities2[rs.types.REDIS_SET]) === 6); 98 | assert(keyCount(receivedEntities[rs.types.REDIS_ZSET]) === 4); 99 | assert(keyCount(receivedEntities2[rs.types.REDIS_ZSET]) === 4); 100 | assert(keyCount(receivedEntities[rs.types.REDIS_HASH]) === 3); 101 | assert(keyCount(receivedEntities2[rs.types.REDIS_HASH]) === 3); 102 | var files = ['stringCommands.txt', 'hashCommands.txt', 'listCommands.txt', 'setCommands.txt', 'zsetCommands.txt', 'keyCommands.txt']; 103 | exec(files.map(function(n) { return 'redis-cli -x <./tests/'+ n; }).join(';')); 104 | } 105 | if(pingCount === 2) { 106 | var k; 107 | for(k in expectedCommandCounts) { 108 | assert.strictEqual(commandCounts[k], expectedCommandCounts[k], 'got '+ (commandCounts[k] || 0)+ ' '+ k+ ', expected '+ expectedCommandCounts[k]); 109 | } 110 | for(k in commandCounts) { 111 | if(commandCounts[k] > 0) { 112 | assert(expectedCommandCounts[k], 'got '+ commandCounts[k] + ' unexpected '+ k); 113 | } 114 | } 115 | console.log('OK'); 116 | process.exit(0); 117 | } 118 | }); 119 | 120 | sync.on('error', function(err) { 121 | console.error(err); 122 | }); 123 | 124 | sync.on('end', function() { 125 | }); 126 | 127 | sync.connect(); 128 | -------------------------------------------------------------------------------- /tests/zsetCommands.txt: -------------------------------------------------------------------------------- 1 | zadd z1 1 a 2 b 3 c 2 | 3 | zincrby z1 10 c 4 | 5 | zrem z1 b 6 | 7 | zadd z2 1 1 2 2 3 3 8 | zadd z3 10001 10002 10003 10003 9 | zadd z4 10000000001 10000000001 10000000002 10000000002 10000000003 10000000003 10 | --------------------------------------------------------------------------------