├── .luacheckrc ├── .busted ├── .luacov ├── spec ├── cqueues-luacov-helper.lua ├── protocol_spec.lua └── cqueues_spec.lua ├── lredis-scm-0.rockspec ├── examples ├── pipelining.lua └── pubsub.lua ├── .travis.yml ├── LICENSE.md ├── lredis ├── commands.lua ├── protocol.lua └── cqueues.lua └── README.md /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | files["spec"] = {std = "+busted"} 3 | -------------------------------------------------------------------------------- /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | lpath = "./?.lua"; 4 | helper = "spec/cqueues-luacov-helper.lua"; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | statsfile = "luacov.stats.out"; 3 | reportfile = "luacov.report.out"; 4 | deletestats = true; 5 | include = { 6 | "/lredis/[^/]+$"; 7 | }; 8 | exclude = { 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /spec/cqueues-luacov-helper.lua: -------------------------------------------------------------------------------- 1 | -- Solves https://github.com/keplerproject/luacov/issues/38 2 | local cqueues = require "cqueues" 3 | local luacov_runner = require "luacov.runner" 4 | local wrap; wrap = cqueues.interpose("wrap", function(self, func, ...) 5 | func = luacov_runner.with_luacov(func) 6 | return wrap(self, func, ...) 7 | end) 8 | -------------------------------------------------------------------------------- /lredis-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lredis" 2 | version = "scm-0" 3 | 4 | description = { 5 | summary = "Redis library for Lua"; 6 | homepage = "https://github.com/daurnimator/lredis"; 7 | license = "MIT/X11"; 8 | } 9 | 10 | source = { 11 | url = "git+https://github.com/daurnimator/lredis.git"; 12 | } 13 | 14 | dependencies = { 15 | "lua >= 5.1"; 16 | "cqueues >= 20150907"; 17 | "fifo"; 18 | } 19 | 20 | build = { 21 | type = "builtin"; 22 | modules = { 23 | ["lredis.commands"] = "lredis/commands.lua"; 24 | ["lredis.cqueues"] = "lredis/cqueues.lua"; 25 | ["lredis.protocol"] = "lredis/protocol.lua"; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /examples/pipelining.lua: -------------------------------------------------------------------------------- 1 | -- Connect to local redis 2 | local r = require "lredis.cqueues".connect_tcp() 3 | 4 | -- Create new scheduler 5 | local cqueues = require "cqueues" 6 | local cq = cqueues.new() 7 | 8 | -- Create two coroutines 9 | cq:wrap(function() 10 | -- Tell server to pause for half a second 11 | print("PAUSE", r:client_pause(0.5)) 12 | end) 13 | cq:wrap(function() 14 | -- Sleep a small amount of time so that this thread goes second 15 | cqueues.sleep(0.01) 16 | -- Pipeline a PING command 17 | -- i.e. write it to the socket (and redis will start processing it) 18 | -- but this coroutine will be blocked from reading the reply until previous commands have fully returned 19 | print("PING", r:ping()) 20 | end) 21 | 22 | -- Run scheduler until there is nothing more to do (or an error) 23 | assert(cq:loop()) 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | env: 6 | matrix: 7 | - LUA="lua 5.1" 8 | - LUA="lua 5.2" 9 | - LUA="lua 5.3" 10 | - LUA="luajit @" 11 | - LUA="luajit 2.0" 12 | - LUA="luajit 2.1" 13 | 14 | branches: 15 | only: 16 | - master 17 | 18 | before_install: 19 | - pip install hererocks 20 | - hererocks here -r^ --$LUA 21 | - export PATH=$PATH:$PWD/here/bin 22 | - eval `luarocks path --bin` 23 | - luarocks install luacov-coveralls 24 | - luarocks install busted 25 | 26 | install: 27 | - luarocks install --only-deps lredis-scm-0.rockspec 28 | 29 | script: 30 | - busted -c 31 | 32 | after_success: 33 | - luacov-coveralls -v 34 | 35 | notifications: 36 | email: 37 | on_success: change 38 | on_failure: always 39 | 40 | cache: 41 | directories: 42 | - $HOME/.cache/hererocks 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daurnimator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/pubsub.lua: -------------------------------------------------------------------------------- 1 | local lrc = require "lredis.cqueues" 2 | local cqueues = require "cqueues" 3 | -- Make a new cqueues scheduler 4 | local cq = cqueues.new() 5 | -- Make a thread that prints published messages to stdout 6 | cq:wrap(function() 7 | local r = lrc.connect_tcp() 8 | r:subscribe("quit") 9 | r:psubscribe("b*") 10 | while true do 11 | local item = r:get_next() 12 | if item == nil then break end 13 | -- Can write `for item in r.get_next, r do` instead 14 | -- but that doesn't work in lua5.1/luajit 15 | local message_type = item[1] 16 | if message_type == "message" then 17 | print("Channel:", item[2], "Message:", item[3]) 18 | if item[2] == "quit" then break end 19 | elseif message_type == "pmessage" then 20 | print("Channel:", item[3], "Message:", item[4]) 21 | end 22 | end 23 | end) 24 | -- Make a second thread that publishes events on an interval 25 | cq:wrap(function() 26 | local r = lrc.connect_tcp() 27 | for i=1, 10 do 28 | cqueues.sleep(0.2) 29 | r:call("publish", "bar", tostring(i)) 30 | end 31 | r:call("publish", "quit", "") 32 | end) 33 | -- Start 'main' loop 34 | assert(cq:loop()) 35 | -------------------------------------------------------------------------------- /lredis/commands.lua: -------------------------------------------------------------------------------- 1 | local methods = {} 2 | 3 | function methods:call(...) 4 | local resp = self:pcall(...) 5 | local is_table = type(resp) == "table" 6 | if is_table and resp.err then 7 | error(resp.err, 2) 8 | end 9 | return resp 10 | end 11 | 12 | local function handle_ok_or_err(resp, lvl) 13 | local is_table = type(resp) == "table" 14 | if is_table and resp.ok then 15 | return resp.ok 16 | else 17 | local err 18 | if is_table and resp.err then 19 | err = resp.err 20 | else 21 | err = "unexpected response format" 22 | end 23 | if lvl == nil then 24 | lvl = 2 25 | elseif lvl ~= 0 then 26 | lvl = lvl + 1 27 | end 28 | error(err, lvl) 29 | end 30 | end 31 | 32 | function methods:ping() 33 | local resp = self:pcall("PING") 34 | return handle_ok_or_err(resp) 35 | end 36 | 37 | function methods:client_pause(delay) 38 | local milliseconds = string.format("%d", math.ceil(delay*1000)) 39 | local resp = self:pcall("client", "pause", milliseconds) 40 | return handle_ok_or_err(resp) 41 | end 42 | 43 | function methods:subscribe(...) 44 | self:start_subscription_mode("SUBSCRIBE", ...) 45 | end 46 | 47 | function methods:unsubscribe(...) 48 | self:start_subscription_mode("UNSUBSCRIBE", ...) 49 | end 50 | 51 | function methods:punsubscribe(...) 52 | self:start_subscription_mode("PUNSUBSCRIBE", ...) 53 | end 54 | 55 | function methods:psubscribe(...) 56 | self:start_subscription_mode("PSUBSCRIBE", ...) 57 | end 58 | 59 | function methods:multi() 60 | local resp = self:call("MULTI") 61 | local ret = handle_ok_or_err(resp, 2) 62 | self:start_transaction() 63 | return ret 64 | end 65 | 66 | function methods:exec() 67 | local resp = self:call("EXEC") 68 | self:end_transaction() 69 | return resp 70 | end 71 | 72 | function methods:discard() 73 | local resp = self:call("DISCARD") 74 | local ret = handle_ok_or_err(resp, 2) 75 | self:end_transaction() 76 | return ret 77 | end 78 | 79 | return methods 80 | -------------------------------------------------------------------------------- /spec/protocol_spec.lua: -------------------------------------------------------------------------------- 1 | describe("lredis.protocol module", function() 2 | local protocol = require "lredis.protocol" 3 | local function write_to_temp_file(str) 4 | local file = io.tmpfile() 5 | assert(file:write(str)) 6 | assert(file:flush()) 7 | assert(file:seek("set")) 8 | return file 9 | end 10 | -- Docs at http://redis.io/topics/protocol 11 | it("composes example from docs", function() 12 | -- From "Sending commands to a Redis Server" section 13 | assert.same("*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n", 14 | protocol.encode_request{"LLEN", "mylist"}) 15 | 16 | end) 17 | it("can parse examples from docs", function() 18 | --- From "RESP Arrays" section 19 | -- Empty array 20 | assert.same({}, 21 | protocol.default_read_response(write_to_temp_file "*0\r\n")) 22 | -- an array of two RESP Bulk Strings "foo" and "bar" 23 | assert.same({"foo", "bar"}, 24 | protocol.default_read_response(write_to_temp_file "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n")) 25 | -- an Array of three integers 26 | assert.same({1, 2, 3}, 27 | protocol.default_read_response(write_to_temp_file "*3\r\n:1\r\n:2\r\n:3\r\n")) 28 | -- mixed types: a list of four integers and a bulk string 29 | assert.same({1, 2, 3, 4, "foobar"}, 30 | protocol.default_read_response(write_to_temp_file "*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n")) 31 | -- null 32 | assert.same(protocol.array_null, 33 | protocol.default_read_response(write_to_temp_file "*-1\r\n")) 34 | -- array of arrays 35 | assert.same( 36 | { 37 | {1,2,3}, 38 | { 39 | protocol.status_reply("Foo"); 40 | protocol.error_reply("Bar"); 41 | } 42 | }, 43 | protocol.default_read_response(write_to_temp_file "*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Foo\r\n-Bar\r\n") 44 | ) 45 | 46 | --- From "Null elements in Arrays" section 47 | assert.same( 48 | { "foo", protocol.string_null, "bar" }, 49 | protocol.default_read_response(write_to_temp_file "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n") 50 | ) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis library for Lua 2 | 3 | ## Features 4 | 5 | - Optionally asynchronous 6 | - Compatible with Lua 5.1, 5.2, 5.3 and [LuaJIT](http://luajit.org/) 7 | - [Subscribe (PubSub) mode](http://redis.io/topics/pubsub) 8 | - Automatic pipelining (if you use more than one coroutine) 9 | 10 | ## Why not **_________**? 11 | 12 | - [redis-lua](https://github.com/nrk/redis-lua)? 13 | - Not asynchronous 14 | - Relies on [luasocket](http://www.impa.br/~diego/software/luasocket) 15 | - Architecture doesn't support subscribe mode 16 | - [lluv-redis](https://github.com/moteus/lua-lluv-redis)? 17 | - Requires lluv/libuv 18 | - [lua-resty-redis](https://github.com/openresty/lua-resty-redis)? 19 | - Only works inside of openresty/nginx 20 | - [lua-hiredis](https://github.com/agladysh/lua-hiredis)? 21 | - Not asynchronous 22 | - Relies on hiredis C module 23 | - Architecture doesn't support subscribe mode 24 | - [lua_redis](https://github.com/omrikiei/lua_redis) 25 | - Not asynchronous 26 | - Relies on hiredis C module 27 | - Architecture doesn't support subscribe mode 28 | - [sidereal](https://github.com/silentbicycle/sidereal)? 29 | - Unmaintained 30 | - Asynchronous mode not really composable 31 | - Relies on [luasocket](http://www.impa.br/~diego/software/luasocket) 32 | - [fend-redis](https://github.com/chatid/fend-redis)? 33 | - Unmaintained 34 | - Relies on hiredis C module 35 | - requires ffi 36 | 37 | 38 | # Status 39 | 40 | This project is a work in progress and not ready for production use. 41 | 42 | [![Build Status](https://travis-ci.org/daurnimator/lredis.svg)](https://travis-ci.org/daurnimator/lredis) 43 | [![Coverage Status](https://coveralls.io/repos/daurnimator/lredis/badge.svg?branch=master&service=github)](https://coveralls.io/github/daurnimator/lredis?branch=master) 44 | 45 | 46 | # Installation 47 | 48 | It's recommended to install lredis by using [luarocks](https://luarocks.org/). 49 | This will automatically install run-time lua dependencies for you. 50 | 51 | $ luarocks install --server=http://luarocks.org/dev lredis 52 | 53 | ## Dependencies 54 | 55 | - [cqueues](http://25thandclement.com/~william/projects/cqueues.html) >= 20150907 56 | - [fifo](https://github.com/daurnimator/fifo.lua) 57 | 58 | ### For running tests 59 | 60 | - [luacheck](https://github.com/mpeterv/luacheck) 61 | - [busted](http://olivinelabs.com/busted/) 62 | - [luacov](https://keplerproject.github.io/luacov/) 63 | 64 | 65 | # Development 66 | 67 | ## Getting started 68 | 69 | - Clone the repo: 70 | ``` 71 | $ git clone https://github.com/daurnimator/lredis.git 72 | $ cd lredis 73 | ``` 74 | 75 | - Install dependencies 76 | ``` 77 | $ luarocks install --only-deps lredis-scm-0.rockspec 78 | ``` 79 | 80 | - Lint the code (check for common programming errors) 81 | ``` 82 | $ luacheck . 83 | ``` 84 | 85 | - Run tests and view coverage report ([install tools first](#for-running-tests)) 86 | ``` 87 | $ busted -c 88 | $ luacov && less luacov.report.out 89 | ``` 90 | 91 | - Install your local copy: 92 | ``` 93 | $ luarocks make lredis-scm-0.rockspec 94 | ``` 95 | -------------------------------------------------------------------------------- /lredis/protocol.lua: -------------------------------------------------------------------------------- 1 | -- Documentation on the redis protocol found at http://redis.io/topics/protocol 2 | 3 | 4 | -- Encode a redis bulk string 5 | local function encode_bulk_string(str) 6 | assert(type(str) == "string") 7 | return string.format("$%d\r\n%s\r\n", #str, str) 8 | end 9 | 10 | -- Encode a redis request 11 | -- Requests are always just an array of bulk strings 12 | local function encode_request(arg) 13 | local n = arg.n or #arg 14 | assert(n > 0, "need at least one argument") 15 | local str = { 16 | [0] = string.format("*%d\r\n", n); 17 | } 18 | for i=1, n do 19 | str[i] = encode_bulk_string(arg[i]) 20 | end 21 | return table.concat(str, nil, 0, n) 22 | end 23 | 24 | -- Encode a redis inline command 25 | -- space separated 26 | local function encode_inline(arg) 27 | local n = arg.n or #arg 28 | assert(n > 0, "need at least one argument") 29 | -- inline commands can't start with "*" 30 | assert(arg[1]:sub(1,1) ~= "*", "invalid command for inline command") 31 | -- ensure the arguments do not contain a space character or newline 32 | for i=1, n do 33 | assert(arg[i]:match("^[^\r\n ]*$"), "invalid string for inline command") 34 | end 35 | arg[n+1] = "\r\n" 36 | return table.concat(arg, " ", 1, n+1) 37 | end 38 | 39 | -- Parse a redis response 40 | local function read_response(file, new_status, new_error, string_null, array_null) 41 | local line = assert(file:read("*l")) 42 | assert(line:sub(-1, -1) == "\r", "invalid line ending") 43 | local status, data = line:sub(1, 1), line:sub(2, -2) 44 | if status == "+" then 45 | return new_status(data) 46 | elseif status == "-" then 47 | return new_error(data) 48 | elseif status == ":" then 49 | return assert(tonumber(data, 10), "invalid integer") 50 | elseif status == "$" then 51 | local len = assert(tonumber(data, 10), "invalid bulk string length") 52 | if len == -1 then 53 | return string_null 54 | elseif len > 512*1024*1024 then -- max 512 MB 55 | error("bulk string too large") 56 | else 57 | local str = assert(file:read(len)) 58 | -- should be followed by CRLF 59 | local crlf = assert(file:read(2)) 60 | assert(crlf == "\r\n", "invalid bulk reply") 61 | return str 62 | end 63 | elseif status == "*" then 64 | local len = assert(tonumber(data, 10), "invalid array length") 65 | if len == -1 then 66 | return array_null 67 | else 68 | local arr = {} 69 | for i=1, len do 70 | arr[i] = read_response(file, new_status, new_error, string_null, array_null) 71 | end 72 | return arr 73 | end 74 | else 75 | error("invalid redis status") 76 | end 77 | end 78 | 79 | -- The way lua embedded into redis encodes things: 80 | local function error_reply(message) 81 | return {err = message} 82 | end 83 | local function status_reply(message) 84 | return {ok = message} 85 | end 86 | local string_null = false 87 | local array_null = false 88 | local function default_read_response(file) 89 | return read_response(file, status_reply, error_reply, string_null, array_null) 90 | end 91 | 92 | return { 93 | encode_bulk_string = encode_bulk_string; 94 | encode_request = encode_request; 95 | encode_inline = encode_inline; 96 | 97 | read_response = read_response; 98 | 99 | error_reply = error_reply; 100 | status_reply = status_reply; 101 | string_null = string_null; 102 | array_null = array_null; 103 | default_read_response = default_read_response; 104 | } 105 | -------------------------------------------------------------------------------- /lredis/cqueues.lua: -------------------------------------------------------------------------------- 1 | local protocol = require "lredis.protocol" 2 | local commands = require "lredis.commands" 3 | local cs = require "cqueues.socket" 4 | local cc = require "cqueues.condition" 5 | local new_fifo = require "fifo" 6 | 7 | local pack = table.pack or function(...) return {n = select("#", ...), ...} end 8 | 9 | local methods = setmetatable({}, {__index = commands}) 10 | local mt = { 11 | __index = methods; 12 | } 13 | 14 | local function new(socket) 15 | socket:setmode("b", "b") 16 | socket:setvbuf("full", math.huge) -- 'infinite' buffering; no write locks needed 17 | return setmetatable({ 18 | socket = socket; 19 | fifo = new_fifo(); 20 | subscribes_pending = 0; 21 | subscribed_to = 0; 22 | in_transaction = false; 23 | }, mt) 24 | end 25 | 26 | local function connect_tcp(host, port) 27 | local socket = assert(cs.connect({ 28 | host = host or "127.0.0.1"; 29 | port = port or "6379"; 30 | nodelay = true; 31 | })) 32 | assert(socket:connect()) 33 | return new(socket) 34 | end 35 | 36 | function methods:close() 37 | self.socket:close() 38 | end 39 | 40 | -- call with table arg/return 41 | function methods:pcallt(arg, new_status, new_error, string_null, array_null) 42 | if self.subscribed_to > 0 or (self.subscribes_pending > 0 and not self.in_transaction) then 43 | error("cannot 'call' while in subscribe mode") 44 | end 45 | local cond = cc.new() 46 | local req = protocol.encode_request(arg) 47 | assert(self.socket:write(req)) 48 | assert(self.socket:flush()) 49 | self.fifo:push(cond) 50 | if self.fifo:peek() ~= cond then 51 | cond:wait() 52 | end 53 | local resp = protocol.read_response(self.socket, new_status, new_error, string_null, array_null) 54 | assert(self.fifo:pop() == cond) 55 | -- signal next thing in pipeline 56 | local next, ok = self.fifo:peek() 57 | if ok then 58 | next:signal() 59 | end 60 | return resp 61 | end 62 | 63 | -- call in vararg style 64 | function methods:pcall(...) 65 | return self:pcallt(pack(...), protocol.status_reply, protocol.error_reply, protocol.string_null, protocol.array_null) 66 | end 67 | 68 | -- need locking around sending subscribe, as you won't know 69 | function methods:start_subscription_modet(arg) 70 | if self.in_transaction then -- in a transaction 71 | -- read off "QUEUED" 72 | local resp = self:pcallt(arg, protocol.status_reply, protocol.error_reply, protocol.string_null, protocol.array_null) 73 | assert(type(resp) == "table" and resp.ok == "QUEUED") 74 | else 75 | local req = protocol.encode_request(arg) 76 | assert(self.socket:write(req)) 77 | assert(self.socket:flush()) 78 | end 79 | self.subscribes_pending = self.subscribes_pending + 1 80 | end 81 | 82 | function methods:start_subscription_mode(...) 83 | return self:start_subscription_modet(pack(...)) 84 | end 85 | 86 | function methods:get_next(new_status, new_error, string_null, array_null) 87 | if self.in_transaction or (self.subscribed_to == 0 and self.subscribes_pending == 0) then 88 | return nil, "not in subscribe mode" 89 | end 90 | local resp = protocol.read_response(self.socket, new_status, new_error, string_null, array_null) 91 | local kind = resp[1] 92 | if kind == "subscribe" or kind == "unsubscribe" or kind == "psubscribe" or kind == "punsubscribe" then 93 | self.subscribed_to = resp[3] 94 | self.subscribes_pending = self.subscribes_pending - 1 95 | end 96 | return resp 97 | end 98 | 99 | function methods:start_transaction() 100 | self.in_transaction = true 101 | end 102 | 103 | function methods:end_transaction() 104 | self.in_transaction = false 105 | end 106 | 107 | return { 108 | new = new; 109 | connect_tcp = connect_tcp; 110 | } 111 | -------------------------------------------------------------------------------- /spec/cqueues_spec.lua: -------------------------------------------------------------------------------- 1 | describe("lredis.cqueues module", function() 2 | local lc = require "lredis.cqueues" 3 | local cqueues = require "cqueues" 4 | local cs = require "cqueues.socket" 5 | it(":close closes the socket", function() 6 | local c, s = cs.pair() 7 | local r = lc.new(c) 8 | r:close() 9 | assert.same(nil, s:read()) 10 | s:close() 11 | end) 12 | it(":ping works", function() 13 | local c, s = cs.pair() 14 | local r = lc.new(c) 15 | local cq = cqueues.new() 16 | cq:wrap(function() 17 | assert(r:ping() == "PONG") 18 | end) 19 | cq:wrap(function() 20 | assert(s:xwrite("+PONG\r\n", "bn")) 21 | end) 22 | assert(cq:loop(1)) 23 | assert(cq:empty()) 24 | r:close() 25 | s:close() 26 | end) 27 | it(":ping works outside of coroutine", function() 28 | local c, s = cs.pair() 29 | local r = lc.new(c) 30 | assert(s:xwrite("+PONG\r\n", "bn")) 31 | assert(r:ping() == "PONG") 32 | r:close() 33 | s:close() 34 | end) 35 | it("supports pipelining", function() 36 | local c, s = cs.pair() 37 | local r = lc.new(c) 38 | local cq = cqueues.new() 39 | cq:wrap(function() 40 | assert(r:ping() == "PONG1") 41 | end) 42 | cq:wrap(function() 43 | cqueues.sleep(0.01) 44 | assert(r:ping() == "PONG2") 45 | end) 46 | cq:wrap(function() 47 | cqueues.sleep(0.02) 48 | assert(s:xwrite("+PONG1\r\n", "bn")) 49 | assert(s:xwrite("+PONG2\r\n", "bn")) 50 | end) 51 | assert(cq:loop(1)) 52 | assert(cq:empty()) 53 | r:close() 54 | s:close() 55 | end) 56 | it("supports pubsub", function() 57 | local c, s = cs.pair() 58 | local r = lc.new(c) 59 | local cq = cqueues.new() 60 | cq:wrap(function() 61 | r:subscribe("foo") 62 | assert.same({"subscribe", "foo", 1}, r:get_next()) 63 | assert.same({"publish", "foo", "message"}, r:get_next()) 64 | r:unsubscribe("foo") 65 | assert.same({"unsubscribe", "foo", 0}, r:get_next()) 66 | assert.same(nil, r:get_next()) 67 | end) 68 | cq:wrap(function() 69 | assert(s:xwrite("*3\r\n$9\r\nsubscribe\r\n$3\r\nfoo\r\n:1\r\n", "bn")) 70 | assert(s:xwrite("*3\r\n$7\r\npublish\r\n$3\r\nfoo\r\n$7\r\nmessage\r\n", "bn")) 71 | assert(s:xwrite("*3\r\n$11\r\nunsubscribe\r\n$3\r\nfoo\r\n:0\r\n", "bn")) 72 | end) 73 | assert(cq:loop(1)) 74 | assert(cq:empty()) 75 | r:close() 76 | s:close() 77 | end) 78 | it("supports transactions", function() 79 | local c, s = cs.pair() 80 | local r = lc.new(c) 81 | local cq = cqueues.new() 82 | cq:wrap(function() 83 | assert.same("OK", r:multi()) 84 | assert.same("QUEUED", r:ping()) 85 | assert.same({{ok="PONG"}}, r:exec()) 86 | end) 87 | cq:wrap(function() 88 | assert(s:xwrite("+OK\r\n", "bn")) 89 | assert(s:xwrite("+QUEUED\r\n", "bn")) 90 | assert(s:xwrite("*1\r\n+PONG\r\n", "bn")) 91 | end) 92 | assert(cq:loop(1)) 93 | assert(cq:empty()) 94 | r:close() 95 | s:close() 96 | end) 97 | it("works when you mix pubsub and transactions", function() 98 | local c, s = cs.pair() 99 | local r = lc.new(c) 100 | local cq = cqueues.new() 101 | cq:wrap(function() 102 | assert.same("OK", r:multi()) 103 | r:subscribe("test") 104 | assert.same({{"subscribe", "test", 1}}, r:exec()) 105 | end) 106 | cq:wrap(function() 107 | assert(s:xwrite("+OK\r\n", "bn")) 108 | assert(s:xwrite("+QUEUED\r\n", "bn")) 109 | assert(s:xwrite("*1\r\n*3\r\n$9\r\nsubscribe\r\n$4\r\ntest\r\n:1\r\n", "bn")) 110 | end) 111 | assert(cq:loop(1)) 112 | assert(cq:empty()) 113 | r:close() 114 | s:close() 115 | end) 116 | it("has working connect_tcp constructor", function() 117 | local m = cs.listen{host="127.0.0.1", port="0"} 118 | local _, host, port = m:localname() 119 | local cq = cqueues.new() 120 | cq:wrap(function() 121 | local r = lc.connect_tcp(host, port) 122 | r:ping() 123 | r:close() 124 | end) 125 | cq:wrap(function() 126 | local s = m:accept() 127 | assert(s:xwrite("+PONG\r\n", "bn")) 128 | s:close() 129 | end) 130 | assert(cq:loop(1)) 131 | assert(cq:empty()) 132 | end) 133 | end) 134 | --------------------------------------------------------------------------------