Skip to content

Commit

Permalink
Lua client added thanks to Daniele Alessandri
Browse files Browse the repository at this point in the history
  • Loading branch information
antirez committed Mar 26, 2009
1 parent e63943a commit f2aa84b
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -6,3 +6,4 @@ redis-benchmark
doc-tools
mkrelease.sh
release
myredis.conf
3 changes: 3 additions & 0 deletions client-libraries/README
Expand Up @@ -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 added client-libraries/lua/.LICENSE.swp
Binary file not shown.
22 changes: 22 additions & 0 deletions client-libraries/lua/LICENSE
@@ -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.
4 changes: 4 additions & 0 deletions client-libraries/lua/README
@@ -0,0 +1,4 @@
redis-lua
-------------------------------------------------------------------------------

A Lua client library for the redis key value storage system.
322 changes: 322 additions & 0 deletions client-libraries/lua/redis.lua
@@ -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
20 changes: 11 additions & 9 deletions redis.conf
Expand Up @@ -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
Expand All @@ -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

0 comments on commit f2aa84b

Please sign in to comment.