Lua client added thanks to Daniele Alessandri

This commit is contained in:
antirez 2009-03-26 17:23:51 +01:00
parent e63943a450
commit f2aa84bd63
7 changed files with 363 additions and 9 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ redis-benchmark
doc-tools
mkrelease.sh
release
myredis.conf

View File

@ -26,4 +26,7 @@ Perl lib source code:
Redis-php PHP C module:
http://code.google.com/p/phpredis/
Lua lib source code:
http://github.com/nrk/redis-lua/tree/master
For all the rest check the Redis tarball or Git repository.

Binary file not shown.

View File

@ -0,0 +1,22 @@
Copyright (c) 2009
Daniele Alessandri
http://www.clorophilla.net/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,4 @@
redis-lua
-------------------------------------------------------------------------------
A Lua client library for the redis key value storage system.

View File

@ -0,0 +1,322 @@
module('Redis', package.seeall)
require('socket') -- requires LuaSocket as a dependency
-- ############################################################################
local protocol = {
newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil',
}
-- ############################################################################
local function toboolean(value)
return value == 1
end
local function _write(self, buffer)
local _, err = self.socket:send(buffer)
if err then error(err) end
end
local function _read(self, len)
if len == nil then len = '*l' end
local line, err = self.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)
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)
end
end
local function _send_raw(self, buffer)
-- TODO: optimize
local bufferType = type(buffer)
if bufferType == 'string' then
_write(self, buffer)
elseif bufferType == 'table' then
_write(self, table.concat(buffer))
else
error('Argument error: ' .. bufferType)
end
return _read_response(self)
end
local function _send_inline(self, command, ...)
if arg.n == 0 then
_write(self, command .. protocol.newline)
else
local arguments = arg
arguments.n = nil
if #arguments > 0 then
arguments = table.concat(arguments, ' ')
else
arguments = ''
end
_write(self, command .. ' ' .. arguments .. protocol.newline)
end
return _read_response(self)
end
local function _send_bulk(self, command, ...)
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
return _send_raw(self, {
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)
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
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
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
end
return number
end
-- ############################################################################
protocol.prefixes = {
['+'] = _read_line,
['-'] = _read_error,
['$'] = _read_bulk,
['*'] = _read_multibulk,
[':'] = _read_integer,
}
-- ############################################################################
local methods = {
-- miscellaneous commands
ping = {
'PING', _send_inline, function(response)
if response == 'PONG' then return true else return false end
end
},
echo = { 'ECHO', _send_bulk },
-- TODO: the server returns an empty -ERR on authentication failure
auth = { 'AUTH' },
-- connection handling
quit = { 'QUIT', function(self, command)
_write(self, 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' },
-- commands operating on the key space
keys = {
'KEYS', _send_inline, 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' },
-- 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' },
-- 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' },
-- multiple databases handling commands
select_database = { 'SELECT' },
move_key = { 'MOVE' },
flush_database = { 'FLUSHDB' },
flush_databases = { 'FLUSHALL' },
-- sorting
--[[
TODO: should we pass sort parameters as a table? e.g:
params = {
by = 'weight_*',
get = 'object_*',
limit = { 0, 10 },
sort = { 'desc', 'alpha' }
}
--]]
sort = { 'SORT' },
-- persistence control commands
save = { 'SAVE' },
background_save = { 'BGSAVE' },
last_save = { 'LASTSAVE' },
shutdown = { 'SHUTDOWN', function(self, command)
_write(self, command .. protocol.newline)
end
},
-- remote server control commands
info = {
'INFO', _send_inline, function(response)
local info = {}
response:gsub('([^\r\n]*)\r\n', function(kv)
local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
info[k] = v
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

View File

@ -62,6 +62,17 @@ databases 16
# slaveof <masterip> <masterport>
################################## SECURITY ###################################
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#requirepass foobared
############################### ADVANCED CONFIG ###############################
# Glue small output buffers together in order to send small replies in a
@ -74,12 +85,3 @@ glueoutputbuf yes
# pool so it uses more CPU and can be a bit slower. Usually it's a good
# idea.
shareobjects no
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#requirepass foobared