]> git.saurik.com Git - redis.git/blame - client-libraries/lua/redis.lua
Better handling of background saving process killed or crashed
[redis.git] / client-libraries / lua / redis.lua
CommitLineData
928394cd 1local _G = _G
2local require, error, type, print = require, error, type, print
3local table, pairs, tostring, tonumber = table, pairs, tostring, tonumber
f2aa84bd 4
928394cd 5module('Redis')
f2aa84bd 6
928394cd 7local socket = require('socket') -- requires LuaSocket as a dependency
f2aa84bd 8
928394cd 9local redis_commands = {}
10local network, request, response, utils = {}, {}, {}, {}, {}
f2aa84bd 11
928394cd 12local protocol = { newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil' }
13
14local function toboolean(value) return value == 1 end
15
16local function load_methods(proto, methods)
17 local redis = _G.setmetatable ({}, _G.getmetatable(proto))
18 for i, v in pairs(proto) do redis[i] = v end
f2aa84bd 19
928394cd 20 for i, v in pairs(methods) do redis[i] = v end
21 return redis
f2aa84bd 22end
23
928394cd 24-- ############################################################################
25
26function network.write(client, buffer)
27 local _, err = client.socket:send(buffer)
f2aa84bd 28 if err then error(err) end
29end
30
928394cd 31function network.read(client, len)
f2aa84bd 32 if len == nil then len = '*l' end
928394cd 33 local line, err = client.socket:receive(len)
f2aa84bd 34 if not err then return line else error('Connection error: ' .. err) end
35end
36
37-- ############################################################################
38
928394cd 39function response.read(client)
40 local res = network.read(client)
f2aa84bd 41 local prefix = res:sub(1, -#res)
42 local response_handler = protocol.prefixes[prefix]
43
44 if not response_handler then
45 error("Unknown response prefix: " .. prefix)
46 else
928394cd 47 return response_handler(client, res)
48 end
49end
50
51function response.status(client, data)
52 local sub = data:sub(2)
53 if sub == protocol.ok then return true else return sub end
54end
55
56function response.error(client, data)
57 local err_line = data:sub(2)
58
59 if err_line:sub(1, 3) == protocol.err then
60 error("Redis error: " .. err_line:sub(5))
61 else
62 error("Redis error: " .. err_line)
63 end
64end
65
66function response.bulk(client, data)
67 local str = data:sub(2)
68 local len = tonumber(str)
69
70 if not len then
71 error('Cannot parse ' .. str .. ' as data length.')
72 else
73 if len == -1 then return nil end
74 local next_chunk = network.read(client, len + 2)
75 return next_chunk:sub(1, -3);
76 end
77end
78
79function response.multibulk(client, data)
80 local str = data:sub(2)
81
82 -- TODO: add a check if the returned value is indeed a number
83 local list_count = tonumber(str)
84
85 if list_count == -1 then
86 return nil
87 else
88 local list = {}
89
90 if list_count > 0 then
91 for i = 1, list_count do
92 table.insert(list, i, response.bulk(client, network.read(client)))
93 end
94 end
95
96 return list
f2aa84bd 97 end
98end
99
928394cd 100function response.integer(client, data)
101 local res = data:sub(2)
102 local number = tonumber(res)
f2aa84bd 103
928394cd 104 if not number then
105 if res == protocol.null then
106 return nil
107 else
108 error('Cannot parse ' .. res .. ' as numeric response.')
109 end
110 end
111
112 return number
113end
114
115protocol.prefixes = {
116 ['+'] = response.status,
117 ['-'] = response.error,
118 ['$'] = response.bulk,
119 ['*'] = response.multibulk,
120 [':'] = response.integer,
121}
122
123-- ############################################################################
124
125function request.raw(client, buffer)
f2aa84bd 126 -- TODO: optimize
127 local bufferType = type(buffer)
128
129 if bufferType == 'string' then
928394cd 130 network.write(client, buffer)
f2aa84bd 131 elseif bufferType == 'table' then
928394cd 132 network.write(client, table.concat(buffer))
f2aa84bd 133 else
134 error('Argument error: ' .. bufferType)
135 end
136
928394cd 137 return response.read(client)
f2aa84bd 138end
139
928394cd 140function request.inline(client, command, ...)
f2aa84bd 141 if arg.n == 0 then
928394cd 142 network.write(client, command .. protocol.newline)
f2aa84bd 143 else
144 local arguments = arg
145 arguments.n = nil
146
147 if #arguments > 0 then
148 arguments = table.concat(arguments, ' ')
149 else
150 arguments = ''
151 end
152
928394cd 153 network.write(client, command .. ' ' .. arguments .. protocol.newline)
f2aa84bd 154 end
155
928394cd 156 return response.read(client)
f2aa84bd 157end
158
928394cd 159function request.bulk(client, command, ...)
f2aa84bd 160 local arguments = arg
161 local data = tostring(table.remove(arguments))
162 arguments.n = nil
163
164 -- TODO: optimize
165 if #arguments > 0 then
166 arguments = table.concat(arguments, ' ')
167 else
168 arguments = ''
169 end
170
928394cd 171 return request.raw(client, {
f2aa84bd 172 command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline
173 })
174end
175
928394cd 176-- ############################################################################
f2aa84bd 177
928394cd 178local function custom(command, send, parse)
179 return function(self, ...)
180 local reply = send(self, command, ...)
181 if parse then
182 return parse(reply, command, ...)
183 else
184 return reply
185 end
f2aa84bd 186 end
187end
188
928394cd 189local function bulk(command, reader)
190 return custom(command, request.bulk, reader)
f2aa84bd 191end
192
928394cd 193local function inline(command, reader)
194 return custom(command, request.inline, reader)
f2aa84bd 195end
196
928394cd 197-- ############################################################################
f2aa84bd 198
928394cd 199function connect(host, port)
200 local client_socket = socket.connect(host, port)
201 if not client_socket then
202 error('Could not connect to ' .. host .. ':' .. port)
f2aa84bd 203 end
204
928394cd 205 local redis_client = {
206 socket = client_socket,
207 raw_cmd = function(self, buffer)
208 return request.raw(self, buffer .. protocol.newline)
209 end,
210 }
f2aa84bd 211
928394cd 212 return load_methods(redis_client, redis_commands)
213end
f2aa84bd 214
215-- ############################################################################
216
928394cd 217redis_commands = {
f2aa84bd 218 -- miscellaneous commands
928394cd 219 ping = inline('PING',
220 function(response)
f2aa84bd 221 if response == 'PONG' then return true else return false end
222 end
928394cd 223 ),
224 echo = bulk('ECHO'),
f2aa84bd 225 -- TODO: the server returns an empty -ERR on authentication failure
928394cd 226 auth = inline('AUTH'),
f2aa84bd 227
228 -- connection handling
928394cd 229 quit = custom('QUIT',
230 function(client, command)
231 -- let's fire and forget! the connection is closed as soon
232 -- as the QUIT command is received by the server.
233 network.write(client, command .. protocol.newline)
f2aa84bd 234 end
928394cd 235 ),
f2aa84bd 236
237 -- commands operating on string values
928394cd 238 set = bulk('SET'),
239 set_preserve = bulk('SETNX', toboolean),
240 get = inline('GET'),
241 get_multiple = inline('MGET'),
242 increment = inline('INCR'),
243 increment_by = inline('INCRBY'),
244 decrement = inline('DECR'),
245 decrement_by = inline('DECRBY'),
246 exists = inline('EXISTS', toboolean),
247 delete = inline('DEL', toboolean),
248 type = inline('TYPE'),
f2aa84bd 249
250 -- commands operating on the key space
928394cd 251 keys = inline('KEYS',
252 function(response)
f2aa84bd 253 local keys = {}
254 response:gsub('%w+', function(key)
255 table.insert(keys, key)
256 end)
257 return keys
258 end
928394cd 259 ),
260 random_key = inline('RANDOMKEY'),
261 rename = inline('RENAME'),
262 rename_preserve = inline('RENAMENX'),
263 expire = inline('EXPIRE', toboolean),
264 database_size = inline('DBSIZE'),
f2aa84bd 265
266 -- commands operating on lists
928394cd 267 push_tail = bulk('RPUSH'),
268 push_head = bulk('LPUSH'),
269 list_length = inline('LLEN'),
270 list_range = inline('LRANGE'),
271 list_trim = inline('LTRIM'),
272 list_index = inline('LINDEX'),
273 list_set = bulk('LSET'),
274 list_remove = bulk('LREM'),
275 pop_first = inline('LPOP'),
276 pop_last = inline('RPOP'),
f2aa84bd 277
278 -- commands operating on sets
928394cd 279 set_add = inline('SADD'),
280 set_remove = inline('SREM'),
281 set_cardinality = inline('SCARD'),
282 set_is_member = inline('SISMEMBER'),
283 set_intersection = inline('SINTER'),
284 set_intersection_store = inline('SINTERSTORE'),
285 set_members = inline('SMEMBERS'),
f2aa84bd 286
287 -- multiple databases handling commands
928394cd 288 select_database = inline('SELECT'),
289 move_key = inline('MOVE'),
290 flush_database = inline('FLUSHDB'),
291 flush_databases = inline('FLUSHALL'),
f2aa84bd 292
293 -- sorting
294 --[[
295 TODO: should we pass sort parameters as a table? e.g:
296 params = {
297 by = 'weight_*',
298 get = 'object_*',
299 limit = { 0, 10 },
300 sort = { 'desc', 'alpha' }
301 }
302 --]]
928394cd 303 sort = custom('SORT',
304 function(client, command, params)
305 -- TODO: here we will put the logic needed to serialize the params
306 -- table to be sent as the argument of the SORT command.
307 return request.inline(client, command, params)
308 end
309 ),
f2aa84bd 310
311 -- persistence control commands
928394cd 312 save = inline('SAVE'),
313 background_save = inline('BGSAVE'),
314 last_save = inline('LASTSAVE'),
315 shutdown = custom('SHUTDOWN',
316 function(client, command)
317 -- let's fire and forget! the connection is closed as soon
318 -- as the SHUTDOWN command is received by the server.
d7fc9edb 319 network.write(client, command .. protocol.newline)
f2aa84bd 320 end
928394cd 321 ),
f2aa84bd 322
323 -- remote server control commands
928394cd 324 info = inline('INFO',
325 function(response)
f2aa84bd 326 local info = {}
327 response:gsub('([^\r\n]*)\r\n', function(kv)
328 local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
329 info[k] = v
330 end)
331 return info
332 end
928394cd 333 ),
f2aa84bd 334}