2009-04-08 07:17:55 -04:00
|
|
|
local _G = _G
|
|
|
|
local require, error, type, print = require, error, type, print
|
|
|
|
local table, pairs, tostring, tonumber = table, pairs, tostring, tonumber
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
module('Redis')
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
local socket = require('socket') -- requires LuaSocket as a dependency
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
local redis_commands = {}
|
|
|
|
local network, request, response, utils = {}, {}, {}, {}, {}
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
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
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
for i, v in pairs(methods) do redis[i] = v end
|
|
|
|
return redis
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
-- ############################################################################
|
|
|
|
|
|
|
|
function network.write(client, buffer)
|
|
|
|
local _, err = client.socket:send(buffer)
|
2009-03-26 12:23:51 -04:00
|
|
|
if err then error(err) end
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
function network.read(client, len)
|
2009-03-26 12:23:51 -04:00
|
|
|
if len == nil then len = '*l' end
|
2009-04-08 07:17:55 -04:00
|
|
|
local line, err = client.socket:receive(len)
|
2009-03-26 12:23:51 -04:00
|
|
|
if not err then return line else error('Connection error: ' .. err) end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- ############################################################################
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
function response.read(client)
|
|
|
|
local res = network.read(client)
|
2009-03-26 12:23:51 -04:00
|
|
|
local prefix = res:sub(1, -#res)
|
|
|
|
local response_handler = protocol.prefixes[prefix]
|
|
|
|
|
|
|
|
if not response_handler then
|
|
|
|
error("Unknown response prefix: " .. prefix)
|
|
|
|
else
|
2009-04-08 07:17:55 -04:00
|
|
|
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
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
function response.integer(client, data)
|
|
|
|
local res = data:sub(2)
|
|
|
|
local number = tonumber(res)
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
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)
|
2009-03-26 12:23:51 -04:00
|
|
|
-- TODO: optimize
|
|
|
|
local bufferType = type(buffer)
|
|
|
|
|
|
|
|
if bufferType == 'string' then
|
2009-04-08 07:17:55 -04:00
|
|
|
network.write(client, buffer)
|
2009-03-26 12:23:51 -04:00
|
|
|
elseif bufferType == 'table' then
|
2009-04-08 07:17:55 -04:00
|
|
|
network.write(client, table.concat(buffer))
|
2009-03-26 12:23:51 -04:00
|
|
|
else
|
|
|
|
error('Argument error: ' .. bufferType)
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
return response.read(client)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
function request.inline(client, command, ...)
|
2009-03-26 12:23:51 -04:00
|
|
|
if arg.n == 0 then
|
2009-04-08 07:17:55 -04:00
|
|
|
network.write(client, command .. protocol.newline)
|
2009-03-26 12:23:51 -04:00
|
|
|
else
|
|
|
|
local arguments = arg
|
|
|
|
arguments.n = nil
|
|
|
|
|
|
|
|
if #arguments > 0 then
|
|
|
|
arguments = table.concat(arguments, ' ')
|
|
|
|
else
|
|
|
|
arguments = ''
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
network.write(client, command .. ' ' .. arguments .. protocol.newline)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
return response.read(client)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
function request.bulk(client, command, ...)
|
2009-03-26 12:23:51 -04:00
|
|
|
local arguments = arg
|
|
|
|
local data = tostring(table.remove(arguments))
|
|
|
|
arguments.n = nil
|
|
|
|
|
|
|
|
-- TODO: optimize
|
|
|
|
if #arguments > 0 then
|
|
|
|
arguments = table.concat(arguments, ' ')
|
|
|
|
else
|
|
|
|
arguments = ''
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
return request.raw(client, {
|
2009-03-26 12:23:51 -04:00
|
|
|
command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
-- ############################################################################
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
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
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
local function bulk(command, reader)
|
|
|
|
return custom(command, request.bulk, reader)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
local function inline(command, reader)
|
|
|
|
return custom(command, request.inline, reader)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
-- ############################################################################
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
function connect(host, port)
|
|
|
|
local client_socket = socket.connect(host, port)
|
|
|
|
if not client_socket then
|
|
|
|
error('Could not connect to ' .. host .. ':' .. port)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
local redis_client = {
|
|
|
|
socket = client_socket,
|
|
|
|
raw_cmd = function(self, buffer)
|
|
|
|
return request.raw(self, buffer .. protocol.newline)
|
|
|
|
end,
|
|
|
|
}
|
2009-03-26 12:23:51 -04:00
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
return load_methods(redis_client, redis_commands)
|
|
|
|
end
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- ############################################################################
|
|
|
|
|
2009-04-08 07:17:55 -04:00
|
|
|
redis_commands = {
|
2009-03-26 12:23:51 -04:00
|
|
|
-- miscellaneous commands
|
2009-04-08 07:17:55 -04:00
|
|
|
ping = inline('PING',
|
|
|
|
function(response)
|
2009-03-26 12:23:51 -04:00
|
|
|
if response == 'PONG' then return true else return false end
|
|
|
|
end
|
2009-04-08 07:17:55 -04:00
|
|
|
),
|
|
|
|
echo = bulk('ECHO'),
|
2009-03-26 12:23:51 -04:00
|
|
|
-- TODO: the server returns an empty -ERR on authentication failure
|
2009-04-08 07:17:55 -04:00
|
|
|
auth = inline('AUTH'),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- connection handling
|
2009-04-08 07:17:55 -04:00
|
|
|
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)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
2009-04-08 07:17:55 -04:00
|
|
|
),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- commands operating on string values
|
2009-04-08 07:17:55 -04:00
|
|
|
set = bulk('SET'),
|
|
|
|
set_preserve = bulk('SETNX', toboolean),
|
|
|
|
get = inline('GET'),
|
|
|
|
get_multiple = inline('MGET'),
|
2009-06-16 10:45:04 -04:00
|
|
|
get_set = bulk('GETSET'),
|
2009-04-08 07:17:55 -04:00
|
|
|
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'),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- commands operating on the key space
|
2009-04-08 07:17:55 -04:00
|
|
|
keys = inline('KEYS',
|
|
|
|
function(response)
|
2009-03-26 12:23:51 -04:00
|
|
|
local keys = {}
|
|
|
|
response:gsub('%w+', function(key)
|
|
|
|
table.insert(keys, key)
|
|
|
|
end)
|
|
|
|
return keys
|
|
|
|
end
|
2009-04-08 07:17:55 -04:00
|
|
|
),
|
|
|
|
random_key = inline('RANDOMKEY'),
|
|
|
|
rename = inline('RENAME'),
|
|
|
|
rename_preserve = inline('RENAMENX'),
|
|
|
|
expire = inline('EXPIRE', toboolean),
|
|
|
|
database_size = inline('DBSIZE'),
|
2009-06-16 10:45:04 -04:00
|
|
|
time_to_live = inline('TTL'),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- commands operating on lists
|
2009-04-08 07:17:55 -04:00
|
|
|
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'),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- commands operating on sets
|
2009-06-16 10:45:04 -04:00
|
|
|
set_add = bulk('SADD'),
|
|
|
|
set_remove = bulk('SREM'),
|
|
|
|
set_move = bulk('SMOVE'),
|
2009-04-08 07:17:55 -04:00
|
|
|
set_cardinality = inline('SCARD'),
|
|
|
|
set_is_member = inline('SISMEMBER'),
|
|
|
|
set_intersection = inline('SINTER'),
|
|
|
|
set_intersection_store = inline('SINTERSTORE'),
|
2009-06-16 10:45:04 -04:00
|
|
|
set_union = inline('SUNION'),
|
|
|
|
set_union_store = inline('SUNIONSTORE'),
|
|
|
|
set_diff = inline('SDIFF'),
|
|
|
|
set_diff_store = inline('SDIFFSTORE'),
|
2009-04-08 07:17:55 -04:00
|
|
|
set_members = inline('SMEMBERS'),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- multiple databases handling commands
|
2009-04-08 07:17:55 -04:00
|
|
|
select_database = inline('SELECT'),
|
|
|
|
move_key = inline('MOVE'),
|
|
|
|
flush_database = inline('FLUSHDB'),
|
|
|
|
flush_databases = inline('FLUSHALL'),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- sorting
|
|
|
|
--[[
|
|
|
|
TODO: should we pass sort parameters as a table? e.g:
|
|
|
|
params = {
|
|
|
|
by = 'weight_*',
|
|
|
|
get = 'object_*',
|
|
|
|
limit = { 0, 10 },
|
|
|
|
sort = { 'desc', 'alpha' }
|
|
|
|
}
|
|
|
|
--]]
|
2009-04-08 07:17:55 -04:00
|
|
|
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
|
|
|
|
),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- persistence control commands
|
2009-04-08 07:17:55 -04:00
|
|
|
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.
|
2009-05-26 12:10:50 -04:00
|
|
|
network.write(client, command .. protocol.newline)
|
2009-03-26 12:23:51 -04:00
|
|
|
end
|
2009-04-08 07:17:55 -04:00
|
|
|
),
|
2009-03-26 12:23:51 -04:00
|
|
|
|
|
|
|
-- remote server control commands
|
2009-06-16 10:45:04 -04:00
|
|
|
info = inline('INFO',
|
2009-04-08 07:17:55 -04:00
|
|
|
function(response)
|
2009-03-26 12:23:51 -04:00
|
|
|
local info = {}
|
|
|
|
response:gsub('([^\r\n]*)\r\n', function(kv)
|
|
|
|
local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
|
|
|
|
info[k] = v
|
|
|
|
end)
|
|
|
|
return info
|
|
|
|
end
|
2009-04-08 07:17:55 -04:00
|
|
|
),
|
2009-06-16 10:45:04 -04:00
|
|
|
slave_of = inline('SLAVEOF'),
|
|
|
|
slave_of_no_one = custom('SLAVEOF',
|
|
|
|
function(client, command)
|
|
|
|
return request.inline(client, command, 'NO ONE')
|
|
|
|
end
|
|
|
|
),
|
2009-03-26 12:23:51 -04:00
|
|
|
}
|