From 928394cd12eb75c35eac72952b29e4dc9e75d154 Mon Sep 17 00:00:00 2001 From: antirez Date: Wed, 8 Apr 2009 13:17:55 +0200 Subject: [PATCH 1/1] Lua client updated --- client-libraries/README | 1 + client-libraries/lua/redis.lua | 398 +++++++++++++++++---------------- 2 files changed, 206 insertions(+), 193 deletions(-) diff --git a/client-libraries/README b/client-libraries/README index f35b74a4..6b824d1f 100644 --- a/client-libraries/README +++ b/client-libraries/README @@ -28,5 +28,6 @@ http://code.google.com/p/phpredis/ Lua lib source code: http://github.com/nrk/redis-lua/tree/master +git://github.com/nrk/redis-lua.git For all the rest check the Redis tarball or Git repository. diff --git a/client-libraries/lua/redis.lua b/client-libraries/lua/redis.lua index 757aae11..2455ceb3 100644 --- a/client-libraries/lua/redis.lua +++ b/client-libraries/lua/redis.lua @@ -1,65 +1,145 @@ -module('Redis', package.seeall) +local _G = _G +local require, error, type, print = require, error, type, print +local table, pairs, tostring, tonumber = table, pairs, tostring, tonumber -require('socket') -- requires LuaSocket as a dependency +module('Redis') --- ############################################################################ +local socket = require('socket') -- requires LuaSocket as a dependency -local protocol = { - newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil', -} +local redis_commands = {} +local network, request, response, utils = {}, {}, {}, {}, {} --- ############################################################################ +local protocol = { newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil' } + +local function toboolean(value) return value == 1 end + +local function load_methods(proto, methods) + local redis = _G.setmetatable ({}, _G.getmetatable(proto)) + for i, v in pairs(proto) do redis[i] = v end -local function toboolean(value) - return value == 1 + for i, v in pairs(methods) do redis[i] = v end + return redis end -local function _write(self, buffer) - local _, err = self.socket:send(buffer) +-- ############################################################################ + +function network.write(client, buffer) + local _, err = client.socket:send(buffer) if err then error(err) end end -local function _read(self, len) +function network.read(client, len) if len == nil then len = '*l' end - local line, err = self.socket:receive(len) + local line, err = client.socket:receive(len) if not err then return line else error('Connection error: ' .. err) end end -- ############################################################################ -local function _read_response(self) - if options and options.close == true then return end - - local res = _read(self) +function response.read(client) + local res = network.read(client) local prefix = res:sub(1, -#res) local response_handler = protocol.prefixes[prefix] if not response_handler then error("Unknown response prefix: " .. prefix) else - return response_handler(self, res) + return response_handler(client, res) + end +end + +function response.status(client, data) + local sub = data:sub(2) + if sub == protocol.ok then return true else return sub end +end + +function response.error(client, data) + local err_line = data:sub(2) + + if err_line:sub(1, 3) == protocol.err then + error("Redis error: " .. err_line:sub(5)) + else + error("Redis error: " .. err_line) + end +end + +function response.bulk(client, data) + local str = data:sub(2) + local len = tonumber(str) + + if not len then + error('Cannot parse ' .. str .. ' as data length.') + else + if len == -1 then return nil end + local next_chunk = network.read(client, len + 2) + return next_chunk:sub(1, -3); + end +end + +function response.multibulk(client, data) + local str = data:sub(2) + + -- TODO: add a check if the returned value is indeed a number + local list_count = tonumber(str) + + if list_count == -1 then + return nil + else + local list = {} + + if list_count > 0 then + for i = 1, list_count do + table.insert(list, i, response.bulk(client, network.read(client))) + end + end + + return list end end +function response.integer(client, data) + local res = data:sub(2) + local number = tonumber(res) -local function _send_raw(self, buffer) + if not number then + if res == protocol.null then + return nil + else + error('Cannot parse ' .. res .. ' as numeric response.') + end + end + + return number +end + +protocol.prefixes = { + ['+'] = response.status, + ['-'] = response.error, + ['$'] = response.bulk, + ['*'] = response.multibulk, + [':'] = response.integer, +} + +-- ############################################################################ + +function request.raw(client, buffer) -- TODO: optimize local bufferType = type(buffer) if bufferType == 'string' then - _write(self, buffer) + network.write(client, buffer) elseif bufferType == 'table' then - _write(self, table.concat(buffer)) + network.write(client, table.concat(buffer)) else error('Argument error: ' .. bufferType) end - return _read_response(self) + return response.read(client) end -local function _send_inline(self, command, ...) +function request.inline(client, command, ...) if arg.n == 0 then - _write(self, command .. protocol.newline) + network.write(client, command .. protocol.newline) else local arguments = arg arguments.n = nil @@ -70,13 +150,13 @@ local function _send_inline(self, command, ...) arguments = '' end - _write(self, command .. ' ' .. arguments .. protocol.newline) + network.write(client, command .. ' ' .. arguments .. protocol.newline) end - return _read_response(self) + return response.read(client) end -local function _send_bulk(self, command, ...) +function request.bulk(client, command, ...) local arguments = arg local data = tostring(table.remove(arguments)) arguments.n = nil @@ -88,169 +168,127 @@ local function _send_bulk(self, command, ...) arguments = '' end - return _send_raw(self, { + return request.raw(client, { command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline }) end +-- ############################################################################ -local function _read_line(self, response) - return response:sub(2) -end - -local function _read_error(self, response) - local err_line = response:sub(2) - - if err_line:sub(1, 3) == protocol.err then - error("Redis error: " .. err_line:sub(5)) - else - error("Redis error: " .. err_line) +local function custom(command, send, parse) + return function(self, ...) + local reply = send(self, command, ...) + if parse then + return parse(reply, command, ...) + else + return reply + end end end -local function _read_bulk(self, response) - local str = response:sub(2) - local len = tonumber(str) - - if not len then - error('Cannot parse ' .. str .. ' as data length.') - else - if len == -1 then return nil end - local data = _read(self, len + 2) - return data:sub(1, -3); - end +local function bulk(command, reader) + return custom(command, request.bulk, reader) end -local function _read_multibulk(self, response) - local str = response:sub(2) - - -- TODO: add a check if the returned value is indeed a number - local list_count = tonumber(str) - - if list_count == -1 then - return nil - else - local list = {} - - if list_count > 0 then - for i = 1, list_count do - table.insert(list, i, _read_bulk(self, _read(self))) - end - end - - return list - end +local function inline(command, reader) + return custom(command, request.inline, reader) end -local function _read_integer(self, response) - local res = response:sub(2) - local number = tonumber(res) +-- ############################################################################ - if not number then - if res == protocol.null then - return nil - else - error('Cannot parse ' .. res .. ' as numeric response.') - end +function connect(host, port) + local client_socket = socket.connect(host, port) + if not client_socket then + error('Could not connect to ' .. host .. ':' .. port) end - return number -end - --- ############################################################################ + local redis_client = { + socket = client_socket, + raw_cmd = function(self, buffer) + return request.raw(self, buffer .. protocol.newline) + end, + } -protocol.prefixes = { - ['+'] = _read_line, - ['-'] = _read_error, - ['$'] = _read_bulk, - ['*'] = _read_multibulk, - [':'] = _read_integer, -} + return load_methods(redis_client, redis_commands) +end -- ############################################################################ -local methods = { +redis_commands = { -- miscellaneous commands - ping = { - 'PING', _send_inline, function(response) + ping = inline('PING', + function(response) if response == 'PONG' then return true else return false end end - }, - echo = { 'ECHO', _send_bulk }, + ), + echo = bulk('ECHO'), -- TODO: the server returns an empty -ERR on authentication failure - auth = { 'AUTH' }, + auth = inline('AUTH'), -- connection handling - quit = { 'QUIT', function(self, command) - _write(self, command .. protocol.newline) + quit = custom('QUIT', + function(client, command) + -- let's fire and forget! the connection is closed as soon + -- as the QUIT command is received by the server. + network.write(client, command .. protocol.newline) end - }, + ), -- commands operating on string values - set = { 'SET', _send_bulk }, - set_preserve = { 'SETNX', _send_bulk, toboolean }, - get = { 'GET' }, - get_multiple = { 'MGET' }, - increment = { 'INCR' }, - increment_by = { 'INCRBY' }, - decrement = { 'DECR' }, - decrement_by = { 'DECRBY' }, - exists = { 'EXISTS', _send_inline, toboolean }, - delete = { 'DEL', _send_inline, toboolean }, - type = { 'TYPE' }, + set = bulk('SET'), + set_preserve = bulk('SETNX', toboolean), + get = inline('GET'), + get_multiple = inline('MGET'), + increment = inline('INCR'), + increment_by = inline('INCRBY'), + decrement = inline('DECR'), + decrement_by = inline('DECRBY'), + exists = inline('EXISTS', toboolean), + delete = inline('DEL', toboolean), + type = inline('TYPE'), -- commands operating on the key space - keys = { - 'KEYS', _send_inline, function(response) + keys = inline('KEYS', + function(response) local keys = {} response:gsub('%w+', function(key) table.insert(keys, key) end) return keys end - }, - random_key = { 'RANDOMKEY' }, - rename = { 'RENAME' }, - rename_preserve = { 'RENAMENX' }, - database_size = { 'DBSIZE' }, + ), + random_key = inline('RANDOMKEY'), + rename = inline('RENAME'), + rename_preserve = inline('RENAMENX'), + expire = inline('EXPIRE', toboolean), + database_size = inline('DBSIZE'), -- commands operating on lists - push_tail = { 'RPUSH', _send_bulk }, - push_head = { 'LPUSH', _send_bulk }, - list_length = { 'LLEN', _send_inline, function(response, key) - --[[ TODO: redis seems to return a -ERR when the specified key does - not hold a list value, but this behaviour is not - consistent with the specs docs. This might be due to the - -ERR response paradigm being new, which supersedes the - check for negative numbers to identify errors. ]] - if response == -2 then - error('Key ' .. key .. ' does not hold a list value') - end - return response - end - }, - list_range = { 'LRANGE' }, - list_trim = { 'LTRIM' }, - list_index = { 'LINDEX' }, - list_set = { 'LSET', _send_bulk }, - list_remove = { 'LREM', _send_bulk }, - pop_first = { 'LPOP' }, - pop_last = { 'RPOP' }, + push_tail = bulk('RPUSH'), + push_head = bulk('LPUSH'), + list_length = inline('LLEN'), + list_range = inline('LRANGE'), + list_trim = inline('LTRIM'), + list_index = inline('LINDEX'), + list_set = bulk('LSET'), + list_remove = bulk('LREM'), + pop_first = inline('LPOP'), + pop_last = inline('RPOP'), -- commands operating on sets - set_add = { 'SADD' }, - set_remove = { 'SREM' }, - set_cardinality = { 'SCARD' }, - set_is_member = { 'SISMEMBER' }, - set_intersection = { 'SINTER' }, - set_intersection_store = { 'SINTERSTORE' }, - set_members = { 'SMEMBERS' }, + set_add = inline('SADD'), + set_remove = inline('SREM'), + set_cardinality = inline('SCARD'), + set_is_member = inline('SISMEMBER'), + set_intersection = inline('SINTER'), + set_intersection_store = inline('SINTERSTORE'), + set_members = inline('SMEMBERS'), -- multiple databases handling commands - select_database = { 'SELECT' }, - move_key = { 'MOVE' }, - flush_database = { 'FLUSHDB' }, - flush_databases = { 'FLUSHALL' }, + select_database = inline('SELECT'), + move_key = inline('MOVE'), + flush_database = inline('FLUSHDB'), + flush_databases = inline('FLUSHALL'), -- sorting --[[ @@ -262,20 +300,29 @@ local methods = { sort = { 'desc', 'alpha' } } --]] - sort = { 'SORT' }, + sort = custom('SORT', + function(client, command, params) + -- TODO: here we will put the logic needed to serialize the params + -- table to be sent as the argument of the SORT command. + return request.inline(client, command, params) + end + ), -- persistence control commands - save = { 'SAVE' }, - background_save = { 'BGSAVE' }, - last_save = { 'LASTSAVE' }, - shutdown = { 'SHUTDOWN', function(self, command) - _write(self, command .. protocol.newline) + save = inline('SAVE'), + background_save = inline('BGSAVE'), + last_save = inline('LASTSAVE'), + shutdown = custom('SHUTDOWN', + function(client, command) + -- let's fire and forget! the connection is closed as soon + -- as the SHUTDOWN command is received by the server. + network.write(command .. protocol.newline) end - }, + ), -- remote server control commands - info = { - 'INFO', _send_inline, function(response) + info = inline('INFO', + function(response) local info = {} response:gsub('([^\r\n]*)\r\n', function(kv) local k,v = kv:match(('([^:]*):([^:]*)'):rep(1)) @@ -283,40 +330,5 @@ local methods = { end) return info end - }, + ), } - -function connect(host, port) - local client_socket = socket.connect(host, port) - - if not client_socket then - error('Could not connect to ' .. host .. ':' .. port) - end - - local redis_client = { - socket = client_socket, - raw_cmd = function(self, buffer) - return _send_raw(self, buffer .. protocol.newline) - end, - } - - return setmetatable(redis_client, { - __index = function(self, method) - local redis_meth = methods[method] - if redis_meth then - return function(self, ...) - if not redis_meth[2] then - table.insert(redis_meth, 2, _send_inline) - end - - local response = redis_meth[2](self, redis_meth[1], ...) - if redis_meth[3] then - return redis_meth[3](response, ...) - else - return response - end - end - end - end - }) -end -- 2.45.2