├── LICENSE ├── README.md ├── const.lua └── tarantool.lua /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Dinar Sabitov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-nginx-tarantool 2 | =================== 3 | 4 | Driver for tarantool 1.6 on nginx cosockets 5 | 6 | Introduction 7 | ------------ 8 | 9 | A driver for a NoSQL database in a Lua script [Tarantool](http://tarantool.org/) build on fast nginx cosockets. 10 | 11 | Requires [lua-MessagePack](https://github.com/fperrad/lua-MessagePack). 12 | 13 | Synopsis 14 | ------------ 15 | 16 | ```lua 17 | 18 | tarantool = require("tarantool") 19 | 20 | -- initialize connection 21 | local tar, err = tarantool:new() 22 | 23 | local tar, err = tarantool:new({ connect_now = false }) 24 | local ok, err = tar:connect() 25 | 26 | local tar, err = tarantool:new({ 27 | host = '127.0.0.1', 28 | port = 3301, 29 | user = 'gg_tester', 30 | password = 'pass', 31 | socket_timeout = 2000, 32 | connect_now = true, 33 | }) 34 | 35 | -- requests 36 | local data, err = tar:ping() 37 | local data, err = tar:insert('profiles', { 1, "nick 1" }) 38 | local data, err = tar:insert('profiles', { 2, "nick 2" }) 39 | local data, err = tar:select(2, 0, 3) 40 | local data, err = tar:select('profiles', 'uid', 3) 41 | local data, err = tar:replace('profiles', {3, "nick 33"}) 42 | local data, err = tar:delete('profiles', 3) 43 | local data, err = tar:update('profiles', 'uid', 3, {{ '=', 1, 'nick new' }}) 44 | local data, err = tar:update('profiles', 'uid', 3, {{ '#', 1, 1 }}) 45 | 46 | -- disconnect or set_keepalive at the end 47 | local ok, err = tar:disconnect() 48 | local ok, err = tar:set_keepalive() 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /const.lua: -------------------------------------------------------------------------------- 1 | module("const", package.seeall) 2 | 3 | -- common 4 | GREETING_SIZE = 128 5 | GREETING_SALT_OFFSET = 64 6 | GREETING_SALT_SIZE = 44 7 | HEAD_BODY_LEN_SIZE = 5 8 | REQUEST_PER_CONNECTION = 100000 9 | MAX_LIMIT = 0xFFFFFFFF 10 | 11 | -- default options 12 | HOST = '127.0.0.1' 13 | PORT = 3301 14 | USER = false 15 | PASSWORD = '' 16 | SOCKET_TIMEOUT = 5000 17 | CONNECT_NOW = true 18 | 19 | -- packet codes 20 | OK = 0 21 | SELECT = 1 22 | INSERT = 2 23 | REPLACE = 3 24 | UPDATE = 4 25 | DELETE = 5 26 | CALL = 6 27 | AUTH = 7 28 | EVAL = 8 29 | UPSERT = 9 30 | PING = 64 31 | ERROR_TYPE = 65536 32 | 33 | -- packet keys 34 | TYPE = 0x00 35 | SYNC = 0x01 36 | SPACE_ID = 0x10 37 | INDEX_ID = 0x11 38 | LIMIT = 0x12 39 | OFFSET = 0x13 40 | ITERATOR = 0x14 41 | KEY = 0x20 42 | TUPLE = 0x21 43 | FUNCTION_NAME = 0x22 44 | USER_NAME = 0x23 45 | OPS = 0x28 46 | DATA = 0x30 47 | ERROR = 0x31 48 | 49 | -- default spaces 50 | SPACE_SCHEMA = 272 51 | SPACE_SPACE = 280 52 | SPACE_INDEX = 288 53 | SPACE_FUNC = 296 54 | SPACE_USER = 304 55 | SPACE_PRIV = 312 56 | SPACE_CLUSTER = 320 57 | 58 | -- index info 59 | INDEX_SPACE_PRIMARY = 0 60 | INDEX_SPACE_NAME = 2 61 | INDEX_INDEX_PRIMARY = 0 62 | INDEX_INDEX_NAME = 2 63 | -------------------------------------------------------------------------------- /tarantool.lua: -------------------------------------------------------------------------------- 1 | module("tarantool", package.seeall) 2 | 3 | local mp = require("MessagePack") 4 | local C = require("const") 5 | local string = string 6 | local table = table 7 | local ngx = ngx 8 | local type = type 9 | local ipairs = ipairs 10 | local error = error 11 | 12 | mp.set_integer('unsigned') 13 | 14 | function new(self, params) 15 | local obj = { 16 | host = C.HOST, 17 | port = C.PORT, 18 | user = C.USER, 19 | password = C.PASSWORD, 20 | socket_timeout = C.SOCKET_TIMEOUT, 21 | connect_now = C.CONNECT_NOW, 22 | } 23 | 24 | if params and type(params) == 'table' then 25 | for key, value in pairs(obj) do 26 | if params[key] ~= nil then 27 | obj[key] = params[key] 28 | end 29 | end 30 | end 31 | 32 | local sock, err = ngx.socket.tcp() 33 | if not sock then 34 | return nil, err 35 | end 36 | 37 | if obj.socket_timeout then 38 | sock:settimeout(obj.socket_timeout) 39 | end 40 | 41 | obj.sock = sock 42 | obj._spaces = {} 43 | obj._indexes = {} 44 | obj = setmetatable(obj, { __index = self }) 45 | 46 | if obj.connect_now then 47 | local ok, err = obj:connect() 48 | if not ok then 49 | return nil, err 50 | end 51 | end 52 | 53 | return obj 54 | end 55 | 56 | function wraperr(self,err) 57 | if err then 58 | err = err .. ', server: ' .. self.host .. ':' .. self.port 59 | end 60 | return err 61 | end 62 | 63 | function connect(self, host, port) 64 | if not self.sock then 65 | return nil, "no socket created" 66 | end 67 | 68 | self.host = host or self.host 69 | self.port = tonumber(port or self.port) 70 | 71 | local ok, err = self.sock:connect(self.host, self.port) 72 | if not ok then 73 | return ok, self:wraperr(err) 74 | end 75 | return self:_handshake() 76 | end 77 | 78 | function disconnect(self) 79 | if not self.sock then 80 | return nil, "no socket created" 81 | end 82 | return self.sock:close() 83 | end 84 | 85 | function set_keepalive(self) 86 | if not self.sock then 87 | return nil, "no socket created" 88 | end 89 | local ok, err = self.sock:setkeepalive() 90 | if not ok then 91 | self:disconnect() 92 | return nil, err 93 | end 94 | return ok 95 | end 96 | 97 | function select(self, space, index, key, opts) 98 | if opts == nil then 99 | opts = {} 100 | end 101 | 102 | local spaceno, err = self:_resolve_space(space) 103 | if not spaceno then 104 | return nil, err 105 | end 106 | 107 | local indexno, err = self:_resolve_index(spaceno, index) 108 | if not indexno then 109 | return nil, err 110 | end 111 | 112 | local body = { 113 | [C.SPACE_ID] = spaceno, 114 | [C.INDEX_ID] = indexno, 115 | [C.KEY] = _prepare_key(key) 116 | } 117 | 118 | if opts.limit ~= nil then 119 | body[C.LIMIT] = tonumber(opts.limit) 120 | else 121 | body[C.LIMIT] = C.MAX_LIMIT 122 | end 123 | if opts.offset ~= nil then 124 | body[C.OFFSET] = tonumber(opts.offset) 125 | else 126 | body[C.OFFSET] = 0 127 | end 128 | 129 | if type(opts.iterator) == 'number' then 130 | body[C.ITERATOR] = opts.iterator 131 | end 132 | 133 | local response, err = self:_request({ [ C.TYPE ] = C.SELECT }, body ) 134 | if err then 135 | return nil, err 136 | elseif response and response.code ~= C.OK then 137 | return nil, self:wraperr(response.error or "Internal error") 138 | else 139 | return response.data 140 | end 141 | end 142 | 143 | function insert(self, space, tuple) 144 | local spaceno, err = self:_resolve_space(space) 145 | if not spaceno then 146 | return nil, err 147 | end 148 | 149 | local response, err = self:_request({ [C.TYPE] = C.INSERT }, { [C.SPACE_ID] = spaceno, [C.TUPLE] = tuple }) 150 | if err then 151 | return nil, err 152 | elseif response and response.code ~= C.OK then 153 | return nil, self:wraperr(response.error or "Internal error") 154 | else 155 | return response.data 156 | end 157 | end 158 | 159 | function replace(self, space, tuple) 160 | local spaceno, err = self:_resolve_space(space) 161 | if not spaceno then 162 | return nil, err 163 | end 164 | 165 | local response, err = self:_request({ [C.TYPE] = C.REPLACE }, { [C.SPACE_ID] = spaceno, [C.TUPLE] = tuple }) 166 | if err then 167 | return nil, err 168 | elseif response and response.code ~= C.OK then 169 | return nil, self:wraperr(response.error or "Internal error") 170 | else 171 | return response.data 172 | end 173 | end 174 | 175 | function delete(self, space, key) 176 | local spaceno, err = self:_resolve_space(space) 177 | if not spaceno then 178 | return nil, err 179 | end 180 | 181 | local response, err = self:_request({ [C.TYPE] = C.DELETE }, { [C.SPACE_ID] = spaceno, [C.KEY] = _prepare_key(key) }) 182 | if err then 183 | return nil, err 184 | elseif response and response.code ~= C.OK then 185 | return nil, self:wraperr(response.error or "Internal error") 186 | else 187 | return response.data 188 | end 189 | end 190 | 191 | function update(self, space, index, key, oplist) 192 | local spaceno, err = self:_resolve_space(space) 193 | if not spaceno then 194 | return nil, err 195 | end 196 | 197 | local indexno, err = self:_resolve_index(spaceno, index) 198 | if not indexno then 199 | return nil, err 200 | end 201 | 202 | local response, err = self:_request({ [C.TYPE] = C.UPDATE }, { 203 | [C.SPACE_ID] = spaceno, 204 | [C.INDEX_ID] = indexno, 205 | [C.KEY] = _prepare_key(key), 206 | [C.TUPLE] = oplist, 207 | }) 208 | if err then 209 | return nil, err 210 | elseif response and response.code ~= C.OK then 211 | return nil, self:wraperr(response.error or "Internal error") 212 | else 213 | return response.data 214 | end 215 | end 216 | 217 | function upsert(self, space, tuple, oplist) 218 | local spaceno, err = self:_resolve_space(space) 219 | if not spaceno then 220 | return nil, err 221 | end 222 | 223 | local response, err = self:_request({ [C.TYPE] = C.UPSERT }, { 224 | [C.SPACE_ID] = spaceno, 225 | [C.TUPLE] = tuple, 226 | [C.OPS] = oplist, 227 | }) 228 | if err then 229 | return nil, err 230 | elseif response and response.code ~= C.OK then 231 | return nil, self:wraperr(response.error or "Internal error") 232 | else 233 | return response.data 234 | end 235 | end 236 | 237 | function ping(self) 238 | local response, err = self:_request({ [ C.TYPE ] = C.PING }, {} ) 239 | if err then 240 | return nil, err 241 | elseif response and response.code ~= C.OK then 242 | return nil, self:wraperr(response.error or "Internal error") 243 | else 244 | return "PONG" 245 | end 246 | end 247 | 248 | function call(self, proc, args) 249 | local response, err = self:_request({ [ C.TYPE ] = C.CALL }, { [C.FUNCTION_NAME] = proc, [C.TUPLE] = args } ) 250 | if err then 251 | return nil, err 252 | elseif response and response.code ~= C.OK then 253 | return nil, self:wraperr(response.error or "Internal error") 254 | else 255 | return response.data 256 | end 257 | end 258 | 259 | function _resolve_space(self, space) 260 | if type(space) == 'number' then 261 | return space 262 | elseif type(space) == 'string' then 263 | if self._spaces[space] then 264 | return self._spaces[space] 265 | end 266 | else 267 | return nil, 'Invalid space identificator: ' .. space 268 | end 269 | 270 | local data, err = self:select(C.SPACE_SPACE, C.INDEX_SPACE_NAME, space) 271 | if not data or not data[1] or not data[1][1] or err then 272 | return nil, (err or 'Can\'t find space with identificator: ' .. space) 273 | end 274 | 275 | self._spaces[space] = data[1][1] 276 | return self._spaces[space] 277 | end 278 | 279 | function _resolve_index(self, space, index) 280 | if type(index) == 'number' then 281 | return index 282 | elseif type(index) == 'string' then 283 | if self._indexes[index] then 284 | return self._indexes[index] 285 | end 286 | else 287 | return nil, 'Invalid index identificator: ' .. index 288 | end 289 | 290 | local spaceno, err = self:_resolve_space(space) 291 | if not spaceno then 292 | return nil, err 293 | end 294 | 295 | local data, err = self:select(C.SPACE_INDEX, C.INDEX_INDEX_NAME, { spaceno, index }) 296 | if not data or not data[1] or not data[1][2] or err then 297 | return nil, (err or 'Can\'t find index with identificator: ' .. index) 298 | end 299 | 300 | self._indexes[index] = data[1][2] 301 | return self._indexes[index] 302 | end 303 | 304 | function _handshake(self) 305 | local count, err = self.sock:getreusedtimes() 306 | local greeting, greeting_err 307 | if count == 0 then 308 | greeting, greeting_err = self.sock:receive(C.GREETING_SIZE) 309 | if not greeting or greeting_err then 310 | self.sock:close() 311 | return nil, self:wraperr(greeting_err) 312 | end 313 | self._salt = string.sub(greeting, C.GREETING_SALT_OFFSET + 1) 314 | self._salt = string.sub(ngx.decode_base64(self._salt), 1, 20) 315 | return self:_authenticate() 316 | end 317 | return true 318 | end 319 | 320 | function _authenticate(self) 321 | if not self.user then 322 | return true 323 | end 324 | 325 | local rbody = { [C.USER_NAME] = self.user, [C.TUPLE] = { } } 326 | 327 | local password = self.password or '' 328 | if password ~= '' then 329 | local step_1 = ngx.sha1_bin(self.password) 330 | local step_2 = ngx.sha1_bin(step_1) 331 | local step_3 = ngx.sha1_bin(self._salt .. step_2) 332 | local scramble = _xor(step_1, step_3) 333 | rbody[C.TUPLE] = { "chap-sha1", scramble } 334 | end 335 | 336 | local response, err = self:_request({ [C.TYPE] = C.AUTH }, rbody) 337 | if err then 338 | return nil, err 339 | elseif response and response.code ~= C.OK then 340 | return nil, self:wraperr(response.error or "Internal error") 341 | else 342 | return true 343 | end 344 | end 345 | 346 | function _request(self, header, body) 347 | local sock = self.sock 348 | 349 | if type(header) ~= 'table' then 350 | return nil, 'invlid request header' 351 | end 352 | 353 | self.sync_num = ((self.sync_num or 0) + 1) % C.REQUEST_PER_CONNECTION 354 | if not header[C.SYNC] then 355 | header[C.SYNC] = self.sync_num 356 | else 357 | self.sync_num = header[C.SYNC] 358 | end 359 | local request = _prepare_request(header, body) 360 | local bytes, err = sock:send(request) 361 | if bytes == nil then 362 | sock:close() 363 | return nil, self:wraperr("Failed to send request: " .. err) 364 | end 365 | 366 | local size, err = sock:receive(C.HEAD_BODY_LEN_SIZE) 367 | if not size then 368 | sock:close() 369 | return nil, self:wraperr("Failed to get response size: " .. err) 370 | end 371 | 372 | size = mp.unpack(size) 373 | if not size then 374 | sock:close() 375 | return nil, self:wraperr("Client get response invalid size") 376 | end 377 | 378 | local header_and_body, err = sock:receive(size) 379 | if not header_and_body then 380 | sock:close() 381 | return nil, self:wraperr("Failed to get response header and body: " .. err) 382 | end 383 | 384 | local iterator = mp.unpacker(header_and_body) 385 | local value, res_header = iterator() 386 | if type(res_header) ~= 'table' then 387 | return nil, self:wraperr("Invalid header: " .. type(res_header) .. " (table expected)") 388 | end 389 | if res_header[C.SYNC] ~= self.sync_num then 390 | return nil, self:wraperr("Invalid header SYNC: request: " .. self.sync_num .. " response: " .. res_header[C.SYNC]) 391 | end 392 | 393 | local value, res_body = iterator() 394 | if type(res_body) ~= 'table' then 395 | res_body = {} 396 | end 397 | 398 | return { code = res_header[C.TYPE], data = res_body[C.DATA], error = res_body[C.ERROR] } 399 | end 400 | 401 | function _prepare_request(h, b) 402 | local header = mp.pack(h) 403 | local body = mp.pack(b) 404 | local len = mp.pack(string.len(header) + string.len(body)) 405 | return len .. header .. body 406 | end 407 | 408 | function _xor(str_a, str_b) 409 | function _bxor (a, b) 410 | local r = 0 411 | for i = 0, 31 do 412 | local x = a / 2 + b / 2 413 | if x ~= math.floor(x) then 414 | r = r + 2^i 415 | end 416 | a = math.floor(a / 2) 417 | b = math.floor(b / 2) 418 | end 419 | return r 420 | end 421 | local result = '' 422 | if string.len(str_a) ~= string.len(str_b) then 423 | return 424 | end 425 | for i = 1, string.len(str_a) do 426 | result = result .. string.char(_bxor(string.byte(str_a, i), string.byte(str_b, i))) 427 | end 428 | return result 429 | end 430 | 431 | function _prepare_key(value) 432 | if type(value) == 'table' then 433 | return value 434 | elseif value == nil then 435 | return { } 436 | else 437 | return { value } 438 | end 439 | end 440 | --------------------------------------------------------------------------------