├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── haricot.lua ├── haricot.test.lua └── rockspec ├── haricot-1.0-1.rockspec ├── haricot-1.1-1.rockspec ├── haricot-1.2-1.rockspec └── haricot-scm-1.rockspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | 5 | luacheck: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: checkout 9 | uses: actions/checkout@v3 10 | 11 | - name: luacheck 12 | uses: lunarmodules/luacheck@v1 13 | 14 | tests: 15 | strategy: 16 | matrix: 17 | lua-version: ["5.1.5", "5.2.4", "5.3.6", "5.4.4"] 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: prepare beanstalkd 24 | run: | 25 | sudo apt-get install -y beanstalkd 26 | beanstalkd & 27 | 28 | - name: localua 29 | run: | 30 | curl https://loadk.com/localua.sh -O 31 | chmod +x localua.sh 32 | ./localua.sh .lua "${{ matrix.lua-version }}" 33 | 34 | - name: install lsocket with forked rockspec (5.4 support) 35 | run: | 36 | ./.lua/bin/luarocks install https://raw.githubusercontent.com/catwell/lsocket/master/src/lsocket-1.4.1-1.rockspec 37 | 38 | - name: run tests 39 | run: | 40 | ./.lua/bin/luarocks test 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | localua.sh 2 | .lua/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Haricot CHANGELOG 2 | 3 | ## v1.0 4 | 5 | - First release. 6 | 7 | ## v1.1 8 | 9 | - Better handling of disconnection. 10 | 11 | ## v1.2 12 | 13 | - Support lsocket as an alternative to LuaSocket. 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012-2013 by Moodstocks SAS 2 | Copyright (C) 2014-2022 by Pierre Chapuis 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haricot 2 | 3 | ![CI Status](https://github.com/catwell/haricot/actions/workflows/ci.yml/badge.svg?branch=master) 4 | 5 | ## Presentation 6 | 7 | Haricot is a [Beanstalk](https://beanstalkd.github.io/) client for Lua. 8 | 9 | ## Note about YAML 10 | 11 | Haricot does not decode the YAML data returned by the following methods: 12 | 13 | - stats-job 14 | - stats-tube 15 | - stats 16 | - list-tubes 17 | - list-tubes-watched 18 | 19 | It returns raw YAML. Use your own decoding library if needed. 20 | 21 | ## Dependencies 22 | 23 | Haricot only depends on LuaSocket or lsocket. 24 | 25 | Tests require [cwtest](https://github.com/catwell/cwtest), a YAML parser such as 26 | [tinyyaml](https://luarocks.org/modules/membphis/lua-tinyyaml), [lyaml](https://github.com/gvvaughan/lyaml) or [the one from lubyk](https://github.com/lubyk/yaml/), both LuaSocket and lsocket and a running beanstalkd instance. 27 | 28 | ## Usage 29 | 30 | ### Creating a job 31 | 32 | ```lua 33 | local haricot = require "haricot" 34 | local bs = haricot.new("localhost", 11300) 35 | bs:put(2048, 0, 60, "hello") 36 | ``` 37 | 38 | ### Consuming a job 39 | 40 | ```lua 41 | local haricot = require "haricot" 42 | local bs = haricot.new("localhost", 11300) 43 | local ok, job = bs:reserve(); assert(ok, job) 44 | local id, data = job.id, job.data 45 | print(data) -- "hello" 46 | bs:delete(id) 47 | ``` 48 | 49 | ### More 50 | 51 | See haricot.test.lua. 52 | 53 | ## Copyright 54 | 55 | - Copyright (c) 2012-2013 Moodstocks SAS 56 | - Copyright (c) 2014-2022 Pierre Chapuis 57 | -------------------------------------------------------------------------------- /haricot.lua: -------------------------------------------------------------------------------- 1 | -- NOTES: 2 | -- `job` format: {id=...,data=...} 3 | 4 | --- low level 5 | 6 | local default_cfg = function() 7 | return { max_job_size = 2^16 } 8 | end 9 | 10 | local is_posint = function(x) 11 | return ( type(x) == "number" and math.floor(x) == x and x >= 0 ) 12 | end 13 | 14 | local hyphen = string.byte("-") 15 | local valid_name = function(x) 16 | local n = #x 17 | return ( 18 | type(x) == "string" and 19 | n > 0 and n <= 200 and 20 | x:byte() ~= hyphen and 21 | x:match("^[%w-_+/;.$()]+$") 22 | ) 23 | end 24 | 25 | local luasocket_send = function(s, buf) 26 | return s:send(buf) 27 | end 28 | 29 | local luasocket_recv = function(s, bytes) 30 | return s:receive(bytes) 31 | end 32 | 33 | local luasocket_getline = function(s) 34 | return s:receive("*l") 35 | end 36 | 37 | local luasocket_connect = function(server, port) 38 | local s = (require "socket").tcp() 39 | local ok, err = s:connect(server, port) 40 | if ok then return s else return nil, err end 41 | end 42 | 43 | local luasocket_close = function(s) 44 | s:close() 45 | end 46 | 47 | local luasocket_t = { 48 | send = luasocket_send, 49 | recv = luasocket_recv, 50 | getline = luasocket_getline, 51 | connect = luasocket_connect, 52 | close = luasocket_close, 53 | } 54 | 55 | local lsocket_send = function(s, buf) 56 | local c = #buf 57 | while c > 0 do 58 | local _, wsock = s.lsocket.select(nil, {s.s}) 59 | assert(wsock[1] == s.s) 60 | local sent, err = s.s:send(buf) 61 | if not sent then return nil, err end 62 | c = c - sent 63 | end 64 | end 65 | 66 | local lsocket_recv = function(s, bytes) 67 | local c, r = bytes, {} 68 | while c > 0 do 69 | local rsock = s.lsocket.select({s.s}) 70 | assert(rsock[1] == s.s) 71 | local t = s.s:recv(c) 72 | if not t then return nil end 73 | r[#r+1] = t 74 | c = c - #t 75 | end 76 | return table.concat(r) 77 | end 78 | 79 | local lsocket_getline = function(s) 80 | local r = {} 81 | while true do 82 | local c = lsocket_recv(s, 1) 83 | if not c then return nil end 84 | if c == '\n' then return table.concat(r) end 85 | if c ~= '\r' then r[#r+1] = c end 86 | end 87 | end 88 | 89 | local lsocket_connect = function(server, port) 90 | local r = {lsocket = (require "lsocket")} 91 | local s, err = r.lsocket.connect("tcp", server, port) 92 | if not s then return nil, err end 93 | local _, wsock = r.lsocket.select(nil, {s}) 94 | assert(wsock[1] == s) 95 | r.s = s 96 | s, err = r.s:status() 97 | if not s then return nil, err end 98 | return r 99 | end 100 | 101 | local lsocket_close = function(s) 102 | s.s:close() 103 | end 104 | 105 | local lsocket_t = { 106 | send = lsocket_send, 107 | recv = lsocket_recv, 108 | getline = lsocket_getline, 109 | connect = lsocket_connect, 110 | close = lsocket_close, 111 | } 112 | 113 | local ll_recv = function(self, bytes) 114 | assert(is_posint(bytes)) 115 | return self.mod.recv(self.cnx, bytes) 116 | end 117 | 118 | local ll_send = function(self, buf) 119 | return self.mod.send(self.cnx, buf) 120 | end 121 | 122 | local getline = function(self) 123 | if not self.cnx then return "NOT_CONNECTED" end 124 | return self.mod.getline(self.cnx) or "NOT_CONNECTED" 125 | end 126 | 127 | local mkcmd = function(cmd, ...) 128 | return table.concat({cmd, ...}, " ") .. "\r\n" 129 | end 130 | 131 | local call = function(self, cmd, ...) 132 | if not self.cnx then return "NOT_CONNECTED" end 133 | ll_send(self, mkcmd(cmd, ...)) 134 | return getline(self) 135 | end 136 | 137 | local recv = function(self, bytes) 138 | if not self.cnx then return nil end 139 | local r = ll_recv(self, bytes + 2) 140 | if r then 141 | return r:sub(1, bytes) 142 | else return nil end 143 | end 144 | 145 | local expect_simple = function(res, s) 146 | if res:match(string.format("^%s$", s)) then 147 | return true 148 | else 149 | return false, res 150 | end 151 | end 152 | 153 | local expect_int = function(res, s) 154 | local id = tonumber(res:match(string.format("^%s (%%d+)$", s))) 155 | if id then 156 | return true, id 157 | else 158 | return false, res 159 | end 160 | end 161 | 162 | local expect_data = function(self, res) 163 | local bytes = tonumber(res:match("^OK (%d+)$")) 164 | if bytes then 165 | local data = recv(self, bytes) 166 | if data then 167 | assert(#data == bytes) 168 | return true, data 169 | else 170 | return false, "NOT_CONNECTED" 171 | end 172 | else 173 | return false, res 174 | end 175 | end 176 | 177 | local expect_job_body = function(self, bytes, id) 178 | local data = recv(self, bytes) 179 | if data then 180 | assert(#data == bytes) 181 | return true, {id = id, data = data} 182 | else 183 | return false, "NOT_CONNECTED" 184 | end 185 | end 186 | 187 | --- methods 188 | 189 | -- connection 190 | 191 | local connect = function(self, server, port) 192 | if self.cnx ~= nil then self:disconnect() end 193 | local err 194 | self.cnx, err = self.mod.connect(server, port) 195 | if not self.cnx then return false, err end 196 | return true 197 | end 198 | 199 | local disconnect = function(self) 200 | if self.cnx ~= nil then 201 | self:quit() 202 | self.mod.close(self.cnx) 203 | self.cnx = nil 204 | return true 205 | end 206 | return false, "NOT_CONNECTED" 207 | end 208 | 209 | -- producer 210 | 211 | local put = function(self, pri, delay, ttr, data) 212 | if not self.cnx then return false, "NOT_CONNECTED" end 213 | assert( 214 | is_posint(pri) and pri < 2^32 and 215 | is_posint(delay) and 216 | is_posint(ttr) and ttr > 0 217 | ) 218 | local bytes = #data 219 | assert(bytes < self.cfg.max_job_size) 220 | local cmd = mkcmd("put", pri, delay, ttr, bytes) .. data .. "\r\n" 221 | ll_send(self, cmd) 222 | local res = getline(self) 223 | return expect_int(res, "INSERTED") 224 | end 225 | 226 | local use = function(self, tube) 227 | assert(valid_name(tube)) 228 | local res = call(self, "use", tube) 229 | local ok = res:match("^USING ([%w-_+/;.$()]+)$") 230 | ok = (ok == tube) 231 | if ok then 232 | return true 233 | else 234 | return false, res 235 | end 236 | end 237 | 238 | -- consumer 239 | 240 | local reserve = function(self) 241 | local res = call(self, "reserve") 242 | local id, bytes = res:match("^RESERVED (%d+) (%d+)$") 243 | if id --[[and bytes]] then 244 | id, bytes = tonumber(id), tonumber(bytes) 245 | return expect_job_body(self, bytes, id) 246 | else 247 | return false, res 248 | end 249 | end 250 | 251 | local reserve_with_timeout = function(self, timeout) 252 | assert(is_posint(timeout)) 253 | local res = call(self, "reserve-with-timeout", timeout) 254 | local id, bytes = res:match("^RESERVED (%d+) (%d+)$") 255 | if id --[[and bytes]] then 256 | id, bytes = tonumber(id), tonumber(bytes) 257 | return expect_job_body(self, bytes, id) 258 | else 259 | return expect_simple(res, "TIMED_OUT") 260 | end 261 | end 262 | 263 | local delete = function(self, id) 264 | assert(is_posint(id)) 265 | local res = call(self, "delete", id) 266 | return expect_simple(res, "DELETED") 267 | end 268 | 269 | local release = function(self, id, pri, delay) 270 | assert( 271 | is_posint(id) and 272 | is_posint(pri) and pri < 2^32 and 273 | is_posint(delay) 274 | ) 275 | local res = call(self, "release", id, pri, delay) 276 | return expect_simple(res, "RELEASED") 277 | end 278 | 279 | local bury = function(self, id, pri) 280 | assert( 281 | is_posint(id) and 282 | is_posint(pri) and pri < 2^32 283 | ) 284 | local res = call(self, "bury", id, pri) 285 | return expect_simple(res, "BURIED") 286 | end 287 | 288 | local touch = function(self, id) 289 | assert(is_posint(id)) 290 | local res = call(self, "touch", id) 291 | return expect_simple(res, "TOUCHED") 292 | end 293 | 294 | local watch = function(self, tube) 295 | assert(valid_name(tube)) 296 | local res = call(self, "watch", tube) 297 | return expect_int(res, "WATCHING") 298 | end 299 | 300 | local ignore = function(self, tube) 301 | assert(valid_name(tube)) 302 | local res = call(self, "ignore", tube) 303 | return expect_int(res, "WATCHING") 304 | end 305 | 306 | -- other 307 | 308 | local _peek_result = function(self, res) -- private 309 | local id, bytes = res:match("^FOUND (%d+) (%d+)$") 310 | if id --[[and bytes]] then 311 | id, bytes = tonumber(id), tonumber(bytes) 312 | return expect_job_body(self, bytes, id) 313 | else 314 | return expect_simple(res, "NOT_FOUND") 315 | end 316 | end 317 | 318 | local peek = function(self, id) 319 | assert(is_posint(id)) 320 | local res = call(self, "peek", id) 321 | return _peek_result(self, res) 322 | end 323 | 324 | local make_peek = function(state) 325 | return function(self) 326 | local res = call(self, string.format("peek-%s", state)) 327 | return _peek_result(self, res) 328 | end 329 | end 330 | 331 | local kick = function(self, bound) 332 | assert(is_posint(bound)) 333 | local res = call(self, "kick", bound) 334 | return expect_int(res, "KICKED") 335 | end 336 | 337 | local kick_job = function(self, id) 338 | assert(is_posint(id)) 339 | local res = call(self, "kick-job", id) 340 | return expect_simple(res, "KICKED") 341 | end 342 | 343 | local stats_job = function(self, id) 344 | assert(is_posint(id)) 345 | local res = call(self, "stats-job", id) 346 | return expect_data(self, res) 347 | end 348 | 349 | local stats_tube = function(self, tube) 350 | assert(valid_name(tube)) 351 | local res = call(self, "stats-tube", tube) 352 | return expect_data(self, res) 353 | end 354 | 355 | local stats = function(self) 356 | local res = call(self, "stats") 357 | return expect_data(self, res) 358 | end 359 | 360 | local list_tubes = function(self) 361 | local res = call(self, "list-tubes") 362 | return expect_data(self, res) 363 | end 364 | 365 | local list_tube_used = function(self) 366 | local res = call(self, "list-tube-used") 367 | local tube = res:match("^USING ([%w-_+/;.$()]+)$") 368 | if tube then 369 | return true, tube 370 | else 371 | return false, res 372 | end 373 | end 374 | 375 | local list_tubes_watched = function(self) 376 | local res = call(self, "list-tubes-watched") 377 | return expect_data(self, res) 378 | end 379 | 380 | local quit = function(self) 381 | if not self.cnx then return false, "NOT_CONNECTED" end 382 | ll_send(self, mkcmd("quit")) 383 | return true 384 | end 385 | 386 | local pause_tube = function(self, tube, delay) 387 | assert(valid_name(tube) and is_posint(delay)) 388 | local res = call(self, "pause-tube", tube, delay) 389 | return expect_simple(res, "PAUSED") 390 | end 391 | 392 | --- class 393 | 394 | local methods = { 395 | -- connection 396 | connect = connect, -- (server,port) -> ok,[err] 397 | disconnect = disconnect, -- () -> ok,[err] 398 | -- producer 399 | put = put, -- (pri,delay,ttr,data) -> ok,[id|err] 400 | use = use, -- (tube) -> ok,[err] 401 | -- consumer 402 | reserve = reserve, -- () -> ok,[job|err] 403 | reserve_with_timeout = reserve_with_timeout, -- () -> ok,[job|nil|err] 404 | delete = delete, -- (id) -> ok,[err] 405 | release = release, -- (id,pri,delay) -> ok,[err] 406 | bury = bury, -- (id,pri) -> ok,[err] 407 | touch = touch, -- (id) -> ok,[err] 408 | watch = watch, -- (tube) -> ok,[count|err] 409 | ignore = ignore, -- (tube) -> ok,[count|err] 410 | -- other 411 | peek = peek, -- (id) -> ok,[job|nil|err] 412 | peek_ready = make_peek("ready"), -- () -> ok,[job|nil|err] 413 | peek_delayed = make_peek("delayed"), -- () -> ok,[job|nil|err] 414 | peek_buried = make_peek("buried"), -- () -> ok,[job|nil|err] 415 | kick = kick, -- (bound) -> ok,[count|err] 416 | kick_job = kick_job, -- (id) -> ok,[err] 417 | stats_job = stats_job, -- (id) -> ok,[yaml|err] 418 | stats_tube = stats_tube, -- (tube) -> ok,[yaml|err] 419 | stats = stats, -- () -> ok,[yaml|err] 420 | list_tubes = list_tubes, -- () -> ok,[yaml|err] 421 | list_tube_used = list_tube_used, -- () -> ok,[tube|err] 422 | list_tubes_watched = list_tubes_watched, -- () -> ok,[tube|err] 423 | quit = quit, -- () -> ok 424 | pause_tube = pause_tube, -- (tube,delay) -> ok,[err] 425 | } 426 | 427 | local new = function(server, port, mod) 428 | if not mod then 429 | if pcall(require, "socket") then 430 | mod = luasocket_t 431 | elseif pcall(require, "lsocket") then 432 | mod = lsocket_t 433 | else 434 | error("could not find luasocket or lsocket") 435 | end 436 | end 437 | local r = {mod = mod, cfg = default_cfg()} 438 | local ok, err = connect(r, server, port) 439 | return setmetatable(r, {__index = methods}), ok, err 440 | end 441 | 442 | return { 443 | new = new, -- instance,conn_ok,[err] 444 | mod = { 445 | luasocket = luasocket_t, 446 | lsocket = lsocket_t, 447 | }, 448 | } 449 | -------------------------------------------------------------------------------- /haricot.test.lua: -------------------------------------------------------------------------------- 1 | local cwtest = require "cwtest" 2 | local haricot = require "haricot" 3 | local socket = require "socket" 4 | local gettime = socket.gettime 5 | local sleep = function(s) socket.select(nil, nil, s) end 6 | local pk = function(...) return {...} end 7 | local fmt = string.format 8 | 9 | local HOST, PORT = "127.0.0.1", 11300 10 | 11 | local ok, res, id, t0 12 | local tube1 = "$haricot$-test1" 13 | 14 | --- Find a YAML parser 15 | local yaml 16 | do 17 | local tinyyaml 18 | ok, tinyyaml = pcall(require, "tinyyaml") 19 | if ok then yaml = { load = tinyyaml.parse } end 20 | if not ok then ok, yaml = pcall(require, "lyaml") end 21 | if not ok then ok, yaml = pcall(require, "yaml") end 22 | if not (ok and type(yaml) == "table" and type(yaml.load) == "function") then 23 | yaml = nil 24 | print("warning: no YAML parser found, stats tests will be skipped") 25 | end 26 | end 27 | 28 | local T = cwtest.new() 29 | 30 | local mods, modname = {"luasocket", "lsocket"} 31 | 32 | for i=1,#mods do 33 | 34 | modname = mods[i] 35 | local bs = haricot.new(HOST, PORT, haricot.mod[modname]) 36 | local _n = function(s) return fmt("%s (%s)", s, modname) end 37 | 38 | T:start(_n("basic")); do 39 | T:yes(bs) 40 | T:eq( pk(bs:watch(tube1)), {true, 2} ) 41 | T:eq( pk(bs:ignore("default")), {true, 1} ) 42 | while true do -- empty tube 43 | ok, res = bs:reserve_with_timeout(0) 44 | assert(ok, fmt("Do you have beanstalkd running on %s:%d?", HOST, PORT)) 45 | if res ~= nil then 46 | assert(type(res.id) == "number") 47 | ok = bs:delete(res.id) 48 | assert(ok) 49 | else break end 50 | end 51 | T:yes( bs:use(tube1) ) 52 | while true do -- clean buried jobs 53 | ok, res = bs:peek_buried() 54 | assert(ok) 55 | if res ~= nil then 56 | assert(type(res.id) == "number") 57 | ok = bs:delete(res.id) 58 | assert(ok) 59 | else break end 60 | end 61 | ok, id = bs:put(0, 0, 60, "hello"); T:yes(ok) 62 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 63 | T:yes( bs:delete(id) ) 64 | end; T:done() 65 | 66 | T:start(_n("timeouts")); do 67 | -- reserve with timeout 68 | t0 = gettime() 69 | T:eq( pk(bs:reserve_with_timeout(3)), {true, nil} ) 70 | T:eq( math.floor((gettime()-t0-3)*10), 0 ) -- took about 3s 71 | ok, id = bs:put(0, 0, 60, "hello"); T:yes(ok) 72 | t0 = gettime() 73 | T:eq( pk(bs:reserve_with_timeout(3)), {true, {id = id, data = "hello"}} ) 74 | assert( (gettime()-t0) < 0.1 ) -- did not take 3s 75 | T:yes( bs:delete(id) ) 76 | -- delay 77 | ok, id = bs:put(0, 3, 60, "hello"); T:yes(ok) 78 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 79 | sleep(1.5) 80 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 81 | sleep(2) 82 | T:eq( pk(bs:reserve_with_timeout(0)), {true, {id = id, data = "hello"}} ) 83 | T:yes( bs:delete(id) ) 84 | -- ttr 85 | ok, id = bs:put(0, 0, 2, "hello"); T:yes(ok) 86 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 87 | sleep(1.5) 88 | T:eq( pk(bs:reserve()), {false, "DEADLINE_SOON"} ) 89 | local r 90 | for _=1,5 do 91 | sleep(1) 92 | r = pk(bs:reserve()) 93 | if r[1] then break end 94 | end 95 | T:eq( r, {true, {id = id, data = "hello"}} ) 96 | T:yes( bs:delete(id) ) 97 | -- touching 98 | ok, id = bs:put(0, 0, 2, "hello"); T:yes(ok) 99 | T:eq( pk(bs:reserve()), {true,{id = id, data = "hello"}} ) 100 | sleep(1.5) 101 | T:eq( pk(bs:reserve()), {false, "DEADLINE_SOON"} ) 102 | T:yes( bs:touch(id) ) 103 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 104 | T:yes( bs:delete(id) ) 105 | -- pausing 106 | ok, id = bs:put(0, 0, 2, "hello"); T:yes(ok) 107 | T:yes( bs:pause_tube(tube1, 2) ) 108 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 109 | sleep(1.5) 110 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 111 | sleep(1) 112 | T:eq( pk(bs:reserve_with_timeout(0)), {true, {id = id, data = "hello"}} ) 113 | T:yes( bs:delete(id) ) 114 | end; T:done() 115 | 116 | T:start(_n("priorities")); do 117 | local id1, id2 118 | -- one way 119 | ok, id1 = bs:put(5, 0, 60, "first"); T:yes(ok) 120 | ok, id2 = bs:put(10, 0, 60, "second"); T:yes(ok) 121 | T:eq( pk(bs:reserve()), {true, {id = id1, data = "first"}} ) 122 | T:eq( pk(bs:reserve()), {true, {id = id2, data = "second"}} ) 123 | T:yes( bs:delete(id1) ) 124 | T:yes( bs:delete(id2) ) 125 | -- reverse way 126 | ok, id2 = bs:put(10, 0, 60, "second"); T:yes(ok) 127 | ok, id1 = bs:put(5, 0, 60, "first"); T:yes(ok) 128 | T:eq( pk(bs:reserve()), {true, {id = id1, data = "first"}} ) 129 | T:eq( pk(bs:reserve()), {true, {id = id2, data = "second"}} ) 130 | T:yes( bs:delete(id1) ) 131 | T:yes( bs:delete(id2) ) 132 | -- same priority 133 | ok, id1 = bs:put(5, 0, 60, "first"); T:yes(ok) 134 | ok, id2 = bs:put(5, 0, 60, "second"); T:yes(ok) 135 | T:eq( pk(bs:reserve()), {true, {id = id1, data = "first"}} ) 136 | T:eq( pk(bs:reserve()), {true, {id = id2, data = "second"}} ) 137 | T:yes( bs:delete(id1) ) 138 | T:yes( bs:delete(id2) ) 139 | -- priorities + delay 140 | t0 = gettime() 141 | ok, id1 = bs:put(5, 1, 60, "first"); T:yes(ok) 142 | ok, id2 = bs:put(10, 0, 60, "second"); T:yes(ok) 143 | T:eq( pk(bs:reserve()), {true, {id = id2, data = "second"}} ) 144 | T:eq( pk(bs:reserve()), {true, {id = id1, data = "first"}} ) 145 | T:eq( math.floor((gettime()-t0-1)*10), 0 ) -- took about 1s 146 | T:yes( bs:delete(id1) ) 147 | T:yes( bs:delete(id2) ) 148 | end;T:done() 149 | 150 | T:start(_n("releasing")); do 151 | ok, id = bs:put(0, 0, 60, "hello"); T:yes(ok) 152 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 153 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 154 | T:yes( bs:release(id, 0, 0) ) 155 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 156 | T:yes( bs:delete(id) ) 157 | end; T:done() 158 | 159 | T:start(_n("burying and kicking")); do 160 | ok, id = bs:put(0, 0, 60, "hello"); T:yes(ok) 161 | T:eq( pk(bs:peek_ready()), {true, {id = id, data = "hello"}} ) 162 | T:eq( pk(bs:peek_buried()), {true, nil} ) 163 | T:eq( pk(bs:peek(id)), {true, {id = id, data = "hello"}} ) 164 | T:eq( pk(bs:bury(id, 0)), {false, "NOT_FOUND"} ) 165 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 166 | T:yes( bs:bury(id, 0) ) 167 | T:eq( pk(bs:peek_ready()), {true, nil} ) 168 | T:eq( pk(bs:peek_buried()), {true, {id = id, data = "hello"}} ) 169 | T:eq( pk(bs:peek(id)), {true, {id = id, data = "hello"}} ) 170 | T:eq( pk(bs:reserve_with_timeout(0)), {true, nil} ) 171 | T:eq( pk(bs:kick(10)), {true, 1} ) 172 | T:eq( pk(bs:peek_ready()), {true, {id = id, data = "hello"}} ) 173 | T:eq( pk(bs:peek_buried()), {true, nil} ) 174 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 175 | T:yes( bs:delete(id) ) 176 | end; T:done() 177 | 178 | if yaml then T:start(_n("stats")); do 179 | ok, id = bs:put(0, 0, 60, "hello"); T:yes(ok) 180 | T:eq( pk(bs:peek_ready()), {true, {id = id, data = "hello"}} ) 181 | ok, res = bs:stats_job(id); T:yes(ok and res) 182 | res = yaml.load(res) 183 | T:yes( res and (res.id == id) and (res.tube == tube1) ) 184 | ok, res = bs:stats_tube(tube1); T:yes(ok and res) 185 | res = yaml.load(res) 186 | T:yes( res and (res["current-jobs-ready"] == 1) ) 187 | ok, res = bs:stats(); T:yes(ok and res) 188 | res = yaml.load(res) 189 | T:yes( res and (res["current-jobs-ready"] == 1) ) 190 | ok = bs:delete(id); T:yes(ok) 191 | T:eq( pk(bs:list_tube_used()), {true, tube1} ) 192 | ok, res = bs:list_tubes_watched(); T:yes(ok and res) 193 | res = yaml.load(res) 194 | T:eq( res, {tube1} ) 195 | ok, res = bs:list_tubes(); T:yes(ok and res) 196 | res = yaml.load(res); T:eq( type(res), "table" ) 197 | end; T:done(); end 198 | 199 | -- wrap up 200 | ok, res = bs:reserve_with_timeout(0); assert(ok and (res == nil)) 201 | ok = bs:quit(); assert(ok) 202 | ok, res = bs:reserve(); assert((not ok) and (res == "NOT_CONNECTED")) 203 | 204 | T:start(_n("disconnect")); do 205 | T:yes( bs:disconnect() ) 206 | T:eq( pk(bs:disconnect()), {false, "NOT_CONNECTED"} ) 207 | T:eq( pk(bs:put(0, 0, 60, "hello")), {false, "NOT_CONNECTED"} ) 208 | T:yes( bs:connect(HOST, PORT) ) 209 | if yaml then 210 | T:eq( pk(bs:list_tube_used()), {true, "default"} ) 211 | ok, res = bs:list_tubes_watched(); T:yes(ok and res) 212 | res = yaml.load(res) 213 | T:eq( res, {"default"} ) 214 | end 215 | T:yes( bs:use(tube1) ) 216 | ok, id = bs:put(0, 0, 60, "hello"); T:yes(ok) 217 | T:yes( bs:disconnect() ) 218 | T:yes( bs:connect(HOST, PORT) ) 219 | T:eq( pk(bs:watch(tube1)), {true, 2} ) 220 | T:eq( pk(bs:reserve()), {true, {id = id, data = "hello"}} ) 221 | T:yes( bs:delete(id) ) 222 | T:yes( bs:disconnect() ) 223 | end; T:done() 224 | 225 | end -- mods 226 | 227 | T:exit() 228 | -------------------------------------------------------------------------------- /rockspec/haricot-1.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "haricot" 2 | version = "1.0-1" 3 | 4 | source = { 5 | url = "http://files.catwell.info/code/releases/haricot-1.0.tar.gz", 6 | md5 = "6293bc305e8253f2e18b64a8ed123e9a", 7 | } 8 | 9 | description = { 10 | summary = "A beanstalkd client.", 11 | detailed = [[ 12 | Haricot is a client for Beanstalkd 13 | (http://kr.github.com/beanstalkd/). 14 | ]], 15 | homepage = "http://github.com/catwell/haricot", 16 | license = "MIT/X11", 17 | } 18 | 19 | dependencies = { "lua >= 5.1", "luasocket" } 20 | 21 | build = { 22 | type = "none", 23 | install = { lua = { haricot = "haricot.lua" } }, 24 | copy_directories = {}, 25 | } 26 | -------------------------------------------------------------------------------- /rockspec/haricot-1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "haricot" 2 | version = "1.1-1" 3 | 4 | source = { 5 | url = "git://github.com/catwell/haricot.git", 6 | branch = "v1.1", 7 | } 8 | 9 | description = { 10 | summary = "A beanstalkd client.", 11 | detailed = [[ 12 | Haricot is a client for Beanstalkd 13 | (http://kr.github.com/beanstalkd/). 14 | ]], 15 | homepage = "http://github.com/catwell/haricot", 16 | license = "MIT/X11", 17 | } 18 | 19 | dependencies = { "lua >= 5.1", "luasocket" } 20 | 21 | build = { 22 | type = "none", 23 | install = { lua = { haricot = "haricot.lua" } }, 24 | copy_directories = {}, 25 | } 26 | -------------------------------------------------------------------------------- /rockspec/haricot-1.2-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "haricot" 2 | version = "1.2-1" 3 | 4 | source = { 5 | url = "git://github.com/catwell/haricot.git", 6 | branch = "v1.2", 7 | } 8 | 9 | description = { 10 | summary = "A beanstalkd client.", 11 | detailed = [[ 12 | Haricot is a client for Beanstalkd (http://kr.github.com/beanstalkd/). 13 | Although this rock requires LuaSocket, lsocket is a supported 14 | alternative. 15 | ]], 16 | homepage = "http://github.com/catwell/haricot", 17 | license = "MIT/X11", 18 | } 19 | 20 | dependencies = { "lua >= 5.1", "luasocket" } 21 | 22 | build = { 23 | type = "none", 24 | install = { lua = { haricot = "haricot.lua" } }, 25 | copy_directories = {}, 26 | } 27 | -------------------------------------------------------------------------------- /rockspec/haricot-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | rockspec_format = "3.0" 2 | 3 | package = "haricot" 4 | version = "scm-1" 5 | 6 | source = { 7 | url = "git://github.com/catwell/haricot.git", 8 | } 9 | 10 | description = { 11 | summary = "A beanstalkd client.", 12 | detailed = [[ 13 | Haricot is a client for Beanstalkd (http://kr.github.com/beanstalkd/). 14 | Although this rock requires LuaSocket, lsocket is a supported 15 | alternative. 16 | ]], 17 | homepage = "http://github.com/catwell/haricot", 18 | license = "MIT/X11", 19 | } 20 | 21 | dependencies = { "lua >= 5.1", "luasocket" } 22 | 23 | build = { 24 | type = "none", 25 | install = { lua = { haricot = "haricot.lua" } }, 26 | copy_directories = {}, 27 | } 28 | 29 | test_dependencies = { 30 | "cwtest", 31 | "penlight", 32 | "luasocket", 33 | "lsocket", 34 | "lua-tinyyaml", 35 | } 36 | 37 | test = { 38 | type = "command", 39 | script = "haricot.test.lua", 40 | } 41 | --------------------------------------------------------------------------------