├── .gitignore ├── LICENSE ├── README └── miniredis.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.db 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010 Benjamin Pollack. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | miniredis is an extremely minimal server that supports exactly enough redis 2 | to power splined. It's currently used in the development version of the 3 | Kiln backend when Kiln runs on Windows computers. While I'm happy to accept 4 | patches making this closer to a real Redis alternative, I currently have 5 | no plans to implement the entire Redis suite. 6 | -------------------------------------------------------------------------------- /miniredis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) 2010 Benjamin Pollack. All rights reserved. 3 | 4 | from __future__ import with_statement 5 | 6 | import datetime 7 | import errno 8 | import getopt 9 | import os 10 | import re 11 | import pickle 12 | import select 13 | import signal 14 | import socket 15 | import sys 16 | import time 17 | 18 | from collections import deque 19 | 20 | class RedisConstant(object): 21 | def __init__(self, type): 22 | self.type = type 23 | 24 | def __repr__(self): 25 | return '' % self.type 26 | 27 | class RedisMessage(object): 28 | def __init__(self, message): 29 | self.message = message 30 | 31 | def __str__(self): 32 | return '+%s' % self.message 33 | 34 | def __repr__(self): 35 | return '' % self.message 36 | 37 | class RedisError(RedisMessage): 38 | def __init__(self, message): 39 | self.message = message 40 | 41 | def __str__(self): 42 | return '-ERR %s' % self.message 43 | 44 | def __repr__(self): 45 | return '' % self.message 46 | 47 | 48 | EMPTY_SCALAR = RedisConstant('EmptyScalar') 49 | EMPTY_LIST = RedisConstant('EmptyList') 50 | BAD_VALUE = RedisError('Operation against a key holding the wrong kind of value') 51 | 52 | 53 | class RedisClient(object): 54 | def __init__(self, socket): 55 | self.socket = socket 56 | self.wfile = socket.makefile('wb') 57 | self.rfile = socket.makefile('rb') 58 | self.db = None 59 | self.table = None 60 | 61 | class MiniRedis(object): 62 | def __init__(self, host='127.0.0.1', port=6379, log_file=None, db_file=None): 63 | super(MiniRedis, self).__init__() 64 | self.host = host 65 | self.port = port 66 | if log_file: 67 | self.log_name = log_file 68 | self.log_file = open(self.log_name, 'w') 69 | else: 70 | self.log_name = None 71 | self.log_file = sys.stdout 72 | self.halt = True 73 | 74 | self.clients = {} 75 | self.tables = {} 76 | self.db_file = db_file 77 | self.lastsave = int(time.time()) 78 | 79 | self.load() 80 | 81 | def dump(self, client, o): 82 | nl = '\r\n' 83 | if isinstance(o, bool): 84 | if o: 85 | client.wfile.write('+OK\r\n') 86 | # Show nothing for a false return; that means be quiet 87 | elif o == EMPTY_SCALAR: 88 | client.wfile.write('$-1\r\n') 89 | elif o == EMPTY_LIST: 90 | client.wfile.write('*-1\r\n') 91 | elif isinstance(o, int): 92 | client.wfile.write(':' + str(o) + nl) 93 | elif isinstance(o, str): 94 | client.wfile.write('$' + str(len(o)) + nl) 95 | client.wfile.write(o + nl) 96 | elif isinstance(o, list): 97 | client.wfile.write('*' + str(len(o)) + nl) 98 | for val in o: 99 | self.dump(client, str(val)) 100 | elif isinstance(o, RedisMessage): 101 | client.wfile.write('%s\r\n' % o) 102 | else: 103 | client.wfile.write('return type not yet implemented\r\n') 104 | client.wfile.flush() 105 | 106 | def load(self): 107 | if self.db_file and os.path.lexists(self.db_file): 108 | with open(self.db_file, 'rb') as f: 109 | self.tables = pickle.load(f) 110 | self.log(None, 'loaded database from file "%s"' % self.db_file) 111 | 112 | def log(self, client, s): 113 | try: 114 | who = '%s:%s' % client.socket.getpeername() if client else 'SERVER' 115 | except: 116 | who = '' 117 | self.log_file.write('%s - %s: %s\n' % (datetime.datetime.now(), who, s)) 118 | self.log_file.flush() 119 | 120 | def handle(self, client): 121 | line = client.rfile.readline() 122 | if not line: 123 | self.log(client, 'client disconnected') 124 | del self.clients[client.socket] 125 | client.socket.close() 126 | return 127 | items = int(line[1:].strip()) 128 | args = [] 129 | for x in xrange(0, items): 130 | length = int(client.rfile.readline().strip()[1:]) 131 | args.append(client.rfile.read(length)) 132 | client.rfile.read(2) # throw out newline 133 | command = args[0].lower() 134 | self.dump(client, getattr(self, 'handle_' + command)(client, *args[1:])) 135 | 136 | def rotate(self): 137 | self.log_file.close() 138 | self.log_file = open(self.log_name, 'w') 139 | 140 | def run(self): 141 | self.halt = False 142 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 143 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 144 | server.bind((self.host, self.port)) 145 | server.listen(5) 146 | while not self.halt: 147 | try: 148 | readable, _, _ = select.select([server] + self.clients.keys(), [], [], 1.0) 149 | except select.error, e: 150 | if e.args[0] == errno.EINTR: 151 | continue 152 | raise 153 | for sock in readable: 154 | if sock == server: 155 | (client_socket, address) = server.accept() 156 | client = RedisClient(client_socket) 157 | self.clients[client_socket] = client 158 | self.log(client, 'client connected') 159 | self.select(client, 0) 160 | else: 161 | try: 162 | self.handle(self.clients[sock]) 163 | except Exception, e: 164 | self.log(client, 'exception: %s' % e) 165 | self.handle_quit(client) 166 | for client_socket in self.clients.iterkeys(): 167 | client_socket.close() 168 | self.clients.clear() 169 | server.close() 170 | 171 | def save(self): 172 | if self.db_file: 173 | with open(self.db_file, 'wb') as f: 174 | pickle.dump(self.tables, f, pickle.HIGHEST_PROTOCOL) 175 | self.lastsave = int(time.time()) 176 | 177 | def select(self, client, db): 178 | if db not in self.tables: 179 | self.tables[db] = {} 180 | client.db = db 181 | client.table = self.tables[db] 182 | 183 | def stop(self): 184 | if not self.halt: 185 | self.log(None, 'STOPPING') 186 | self.save() 187 | self.halt = True 188 | 189 | # HANDLERS 190 | 191 | def handle_bgsave(self, client): 192 | if hasattr(os, 'fork'): 193 | if not os.fork(): 194 | self.save() 195 | sys.exit(0) 196 | else: 197 | self.save() 198 | self.log(client, 'BGSAVE') 199 | return RedisMessage('Background saving started') 200 | 201 | def handle_decr(self, client, key): 202 | return self.handle_decrby(self, client, key, 1) 203 | 204 | def handle_decrby(self, client, key, by): 205 | return self.handle_incrby(self, client, key, -by) 206 | 207 | def handle_del(self, client, key): 208 | self.log(client, 'DEL %s' % key) 209 | if key not in client.table: 210 | return 0 211 | del client.table[key] 212 | return 1 213 | 214 | def handle_flushdb(self, client): 215 | self.log(client, 'FLUSHDB') 216 | client.table.clear() 217 | return True 218 | 219 | def handle_flushall(self, client): 220 | self.log(client, 'FLUSHALL') 221 | for table in self.tables.itervalues(): 222 | table.clear() 223 | return True 224 | 225 | def handle_get(self, client, key): 226 | data = client.table.get(key, None) 227 | if isinstance(data, deque): 228 | return BAD_VALUE 229 | if data != None: 230 | data = str(data) 231 | else: 232 | data = EMPTY_SCALAR 233 | self.log(client, 'GET %s -> %s' % (key, data)) 234 | return data 235 | 236 | def handle_incr(self, client, key): 237 | return self.handle_incrby(client, key, 1) 238 | 239 | def handle_incrby(self, client, key, by): 240 | try: 241 | client.table[key] = int(client.table[key]) 242 | client.table[key] += int(by) 243 | except (KeyError, TypeError, ValueError): 244 | client.table[key] = 1 245 | self.log(client, 'INCRBY %s %s -> %s' % (key, by, client.table[key])) 246 | return client.table[key] 247 | 248 | def handle_keys(self, client, pattern): 249 | r = re.compile(pattern.replace('*', '.*')) 250 | self.log(client, 'KEYS %s' % pattern) 251 | return [k for k in client.table.keys() if r.search(k)] 252 | 253 | def handle_lastsave(self, client): 254 | return self.lastsave 255 | 256 | def handle_llen(self, client, key): 257 | if key not in client.table: 258 | return 0 259 | if not isinstance(client.table[key], deque): 260 | return BAD_VALUE 261 | return len(client.table[key]) 262 | 263 | def handle_lpop(self, client, key): 264 | if key not in client.table: 265 | return EMPTY_SCALAR 266 | if not isinstance(client.table[key], deque): 267 | return BAD_VALUE 268 | if len(client.table[key]) > 0: 269 | data = client.table[key].popleft() 270 | else: 271 | data = EMPTY_SCALAR 272 | self.log(client, 'LPOP %s -> %s' % (key, data)) 273 | return data 274 | 275 | def handle_lpush(self, client, key, data): 276 | if key not in client.table: 277 | client.table[key] = deque() 278 | elif not isinstance(client.table[key], deque): 279 | return BAD_VALUE 280 | client.table[key].appendleft(data) 281 | self.log(client, 'LPUSH %s %s' % (key, data)) 282 | return True 283 | 284 | def handle_lrange(self, client, key, low, high): 285 | low, high = int(low), int(high) 286 | if low == 0 and high == -1: 287 | high = None 288 | if key not in client.table: 289 | return EMPTY_LIST 290 | if not isinstance(client.table[key], deque): 291 | return BAD_VALUE 292 | l = list(client.table[key])[low:high] 293 | self.log(client, 'LRANGE %s %s %s -> %s' % (key, low, high, l)) 294 | return l 295 | 296 | def handle_ping(self, client): 297 | self.log(client, 'PING -> PONG') 298 | return RedisMessage('PONG') 299 | 300 | def handle_rpop(self, client, key): 301 | if key not in client.table: 302 | return EMPTY_SCALAR 303 | if not isinstance(client.table[key], deque): 304 | return BAD_VALUE 305 | if len(client.table[key]) > 0: 306 | data = client.table[key].pop() 307 | else: 308 | data = EMPTY_SCALAR 309 | self.log(client, 'RPOP %s -> %s' % (key, data)) 310 | return data 311 | 312 | def handle_rpush(self, client, key, data): 313 | if key not in client.table: 314 | client.table[key] = deque() 315 | elif not isinstance(client.table[key], deque): 316 | return BAD_VALUE 317 | client.table[key].append(data) 318 | self.log(client, 'RPUSH %s %s' % (key, data)) 319 | return True 320 | 321 | def handle_type(self, client, key): 322 | if key not in client.table: 323 | return RedisMessage('none') 324 | 325 | data = client.table[key] 326 | if isinstance(data, deque): 327 | return RedisMessage('list') 328 | elif isinstance(data, set): 329 | return RedisMessage('set') 330 | elif isinstance(data, dict): 331 | return RedisMessage('hash') 332 | elif isinstance(data, str): 333 | return RedisMessage('string') 334 | else: 335 | return RedisError('unknown data type') 336 | 337 | def handle_quit(self, client): 338 | client.socket.shutdown(socket.SHUT_RDWR) 339 | client.socket.close() 340 | self.log(client, 'QUIT') 341 | del self.clients[client.socket] 342 | return False 343 | 344 | def handle_save(self, client): 345 | self.save() 346 | self.log(client, 'SAVE') 347 | return True 348 | 349 | def handle_select(self, client, db): 350 | db = int(db) 351 | self.select(client, db) 352 | self.log(client, 'SELECT %s' % db) 353 | return True 354 | 355 | def handle_set(self, client, key, data): 356 | client.table[key] = data 357 | self.log(client, 'SET %s -> %s' % (key, data)) 358 | return True 359 | 360 | def handle_setnx(self, client, key, data): 361 | if key in client.table: 362 | self.log(client, 'SETNX %s -> %s FAILED' % (key, data)) 363 | return 0 364 | client.table[key] = data 365 | self.log(client, 'SETNX %s -> %s' % (key, data)) 366 | return 1 367 | 368 | def handle_shutdown(self, client): 369 | self.log(client, 'SHUTDOWN') 370 | self.halt = True 371 | self.save() 372 | return self.handle_quit(client) 373 | 374 | def main(args): 375 | if os.name == 'posix': 376 | def sigterm(signum, frame): 377 | m.stop() 378 | def sighup(signum, frame): 379 | m.rotate() 380 | signal.signal(signal.SIGTERM, sigterm) 381 | signal.signal(signal.SIGHUP, sighup) 382 | 383 | host, port, log_file, db_file = '127.0.0.1', 6379, None, None 384 | opts, args = getopt.getopt(args, 'h:p:d:l:f:') 385 | pid_file = None 386 | for o, a in opts: 387 | if o == '-h': 388 | host = a 389 | elif o == '-p': 390 | port = int(a) 391 | elif o == '-l': 392 | log_file = os.path.abspath(a) 393 | elif o == '-d': 394 | db_file = os.path.abspath(a) 395 | elif o == '-f': 396 | pid_file = os.path.abspath(a) 397 | if pid_file: 398 | with open(pid_file, 'w') as f: 399 | f.write('%s\n' % os.getpid()) 400 | m = MiniRedis(host=host, port=port, log_file=log_file, db_file=db_file) 401 | try: 402 | m.run() 403 | except KeyboardInterrupt: 404 | m.stop() 405 | if pid_file: 406 | os.unlink(pid_file) 407 | sys.exit(0) 408 | 409 | if __name__ == '__main__': 410 | main(sys.argv[1:]) 411 | --------------------------------------------------------------------------------