├── .gitignore ├── .travis.yml ├── Makefile ├── README.rus.md ├── docker ├── Dockerfile.in └── start-lp.lua ├── lp-1.0-3.rockspec ├── lp.rockspec.in ├── lp ├── init.lua ├── migrations.lua └── pack_key.lua └── t ├── 010-base.t ├── 015-pack_key.t ├── 020-require.t ├── 030-put.t ├── 040-take.t ├── 050-push-subscribe.t ├── 060-expired.t ├── 100-master-slave.master.lua ├── 100-master-slave.replica.lua ├── 100-master-slave.t └── tnt └── init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | lp-*.rockspec.prepare 2 | *.src.rock 3 | docker/Dockerfile 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | script: 4 | - lsb_release -c -s 5 | - curl -s http://download.tarantool.org/tarantool/1.7/gpgkey | sudo apt-key add - 6 | - sudo apt-get -qq install apt-transport-https 7 | - echo deb https://packagecloud.io/tarantool/1_7/`lsb_release -i -s|perl -p -e 's/.*/lc $&/ge'`/ `lsb_release -c -s` main|sudo tee /etc/apt/sources.list.d/tarantool.list 8 | - sudo apt-get -qq update 9 | - sudo apt-get -qq install tarantool 10 | - tarantool --version 11 | - prove -r t 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERBOSE ?= 0 2 | 3 | VERSION = $(shell grep VERSION lp/init.lua \ 4 | |awk '{print $$3}' \ 5 | |sed "s/[,']//g" \ 6 | ) 7 | 8 | 9 | SPEC_NAME = lp-$(VERSION).rockspec 10 | 11 | DOCKER_VERSIONS = \ 12 | 2.8.3 \ 13 | 2.8 14 | DOCKER_LATEST = 2.8.3 15 | 16 | GITVERSION = $(shell git describe) 17 | 18 | all: 19 | @echo usage: 'make test' 20 | 21 | 22 | test: 23 | @echo '# Run tests for version: $(VERSION)' 24 | prove -r$(shell if test "$(VERBOSE)" -gt 0; then echo v; fi) t 25 | 26 | 27 | update-spec: $(SPEC_NAME) 28 | 29 | 30 | $(SPEC_NAME): $(lp/init.lua) lp.rockspec.in 31 | rm -fr lp-*.rockspec 32 | cp -v lp.rockspec.in $@.prepare 33 | sed -Ei 's/@@VERSION@@/$(VERSION)/g' $@.prepare 34 | mv -v $@.prepare $@ 35 | git add $@ 36 | 37 | 38 | upload: update-spec 39 | rm -f lp-*.src.rock 40 | luarocks upload $(SPEC_NAME) 41 | 42 | 43 | dockers: 44 | @set -e; \ 45 | cd docker; \ 46 | for version in $(DOCKER_VERSIONS); do \ 47 | TAGS="-t unera/tarantool-lp:$$version-$(GITVERSION)"; \ 48 | test $$version = $(DOCKER_LATEST) && TAGS="-t unera/tarantool-lp:latest $$TAGS"; \ 49 | echo "\\nDockers creating: $$TAGS..."; \ 50 | sed -E "s/@@VERSION@@/$$version/g" Dockerfile.in > Dockerfile \ 51 | | docker build . \ 52 | $$TAGS 2>&1 |sed -u -E 's/^/\t/' \ 53 | ; \ 54 | done 55 | 56 | docker-upload: dockers 57 | @set -e; \ 58 | cd docker; \ 59 | for version in $(DOCKER_VERSIONS); do \ 60 | TAGS="unera/tarantool-lp:$$version-$(GITVERSION)"; \ 61 | test $$version = $(DOCKER_LATEST) && TAGS="$$TAGS unera/tarantool-lp:latest"; \ 62 | echo "\\n/ $$version / Uploading: $$TAGS..."; \ 63 | for tag in $$TAGS; do \ 64 | echo + docker push $$tag; \ 65 | docker push $$tag; \ 66 | done; \ 67 | done 68 | 69 | 70 | .PHONY: \ 71 | all \ 72 | test \ 73 | update-spec \ 74 | clean 75 | 76 | -------------------------------------------------------------------------------- /README.rus.md: -------------------------------------------------------------------------------- 1 | # Введение 2 | 3 | Данный набор скриптов предназначен для серверной поддержки системы доставки 4 | сообщений клиентам о происходящих событиях. В качестве транспорта возможно 5 | использовать механизмы long polling или websocket. 6 | 7 | Сервер, поддерживающий данные протоколы не входит в данный репозитарий. 8 | Здесь только часть связанная с хранением/доставкой данных. 9 | 10 | 11 | ## Масштабирование 12 | 13 | Несмотря на то, что тарантул поддерживает асинхронную мастер-мастер, однако 14 | делать push можно только в один инстанс. Subscribe можно делать на любом 15 | инстансе. Таким образом при увеличении нагрузки нужно просто добавлять 16 | инстансы-реплики которые будут обслуживать клиентов, которые принимают 17 | сообщения, а все генерируемые сообщения отправлять на выделенный инстанс. 18 | 19 | 20 | ## Работа 21 | 22 | Для работы с данным лонгпулингом требуется 23 | 24 | 1. драйвер к тарантулу, поддерживающий асинхронный доступ (множество запросов 25 | по одному коннекту). Например [DR::Tnt](https://github.com/dr-co/dr-tnt) 26 | 1. асинхронный вебсервер. Например Twiggy или Coro::Twiggy 27 | 1. Tarantool версии 1.6 и больше 28 | 1. данный набор lua 29 | 30 | # Использование LUA 31 | 32 | ```lua 33 | 34 | lp = require 'lp' 35 | lp:init{ ... } 36 | 37 | 38 | lp:push(key, value) 39 | lp:push_list(key1, value1, key2, value2, ...) 40 | 41 | ... 42 | 43 | local events = lp:subscribe(12345, 25, key1, key2, ...) 44 | 45 | ``` 46 | 47 | # Установка 48 | 49 | Для установки необходимы файлы из директории `lua/` 50 | 51 | 52 | ## Инициализация 53 | 54 | ```lua 55 | lp = require 'lp' 56 | lp:init{ option = value } 57 | ``` 58 | 59 | Опции инициализации: 60 | 61 | * `expire_timeout = 1800` - Максимальное время жизни события в БД 62 | * `serialize_key_method = 'none'` - Способ сериализации ключа 63 | * `lsn_check_interval = 1` - Интервал проверки на наличие новых событий 64 | (данная опция актуальна только для реплик) 65 | * `run_expired = true` - Запускать или нет на данном инстансе процесс удаления 66 | устаревших записей 67 | 68 | 69 | ## Хранение сообщений в базе данных 70 | 71 | Каждое сообщение хранится в виде тапла: 72 | 73 | 1. `id` - идентификатор сообщения (присваивается `push`) 74 | 1. `created` - время когда создано сообщение (используется процессом 75 | expiration) 76 | 1. `key` - ключ сообщения 77 | 1. `data` - данные связанные с сообщением (в обычном случае - сериализованный 78 | (например JSON) объект). 79 | 80 | 81 | Поскольку ключ сообщения требуется индексировать, то использование составных 82 | ключей возможно только при условии включения механизма сериализации ключа. 83 | 84 | Доступные механизмы сериализации: 85 | 86 | * `none` (по умолчанию) - не сериализовать ключ 87 | * `msgpack` - сериализовать при помощи msgpack 88 | * `json` - сериализовать при помощи json 89 | * `colon` - сериализовать в строку при помощи конкатенации через двоеточие 90 | 91 | # API 92 | 93 | ## Добавление сообщения 94 | 95 | Есть два метода, позволяющие добавить одно или несколько сообщений: 96 | 97 | ```lua 98 | 99 | lp:push(key, value) 100 | lp:push_list(key1, value1, key2, value2, ...) 101 | 102 | ``` 103 | 104 | Второй метод просто перевызывает первый в цикле. 105 | 106 | ## Подписка на сообщения 107 | 108 | ```lua 109 | 110 | local list = lp:subscribe(id, timeout, key1, key2, ...) 111 | 112 | ``` 113 | 114 | Подписывается на получение сообщений с ключами `key1`, `key2`, итп. 115 | Подписка осуществляется на интервал времени `timeout`. 116 | 117 | В качестве `id` необходимо передать стартовый `id` с которого 118 | необходимо получать сообщения. `id=0` означает: "ожидать все сообщения, 119 | начиная от текущего времени". 120 | 121 | В списке возвращаются все полученные сообщения по переданным ключам 122 | (если они есть), а так же завершающая запись, содержащая в себе 123 | `id`, который можно использовать для следующего `subscribe`. 124 | 125 | Таким образом алгоритм работы клиента выглядит следующим образом. 126 | 127 | 128 | 1. начало: `id=0` 129 | 2. запрос `lp:subscribe(id, timeout, key1, key2, ...)` 130 | 3. обработка списка полученных сообщений (все элементы полученного списка кроме последнего) 131 | 4. id=id из последнего элемента списка 132 | 5. перейти к шагу 2 133 | 134 | Для случая "лонгпулинг на сайте" пункты 1, 3, 4, 5 делаются на 135 | клиенте при помощи JavaScript. 136 | Пункт 2 делается на сервере при помощи асинхронного вебсервера 137 | (например Twiggy или Node.JS). 138 | 139 | -------------------------------------------------------------------------------- /docker/Dockerfile.in: -------------------------------------------------------------------------------- 1 | FROM tarantool/tarantool:@@VERSION@@ 2 | WORKDIR /opt/lp 3 | COPY start-lp.lua /opt/lp 4 | RUN luarocks --to /opt/.rocks --local install lp 5 | RUN ln -s /opt/.rocks/share/lua/5.1/ /opt/.rocks/share/tarantool 6 | CMD [ "tarantool", "/opt/lp/start-lp.lua" ] 7 | -------------------------------------------------------------------------------- /docker/start-lp.lua: -------------------------------------------------------------------------------- 1 | box.cfg{} 2 | lp = require 'lp' 3 | lp:init { 4 | serialize_key_method = 'json', 5 | } 6 | 7 | -------------------------------------------------------------------------------- /lp-1.0-3.rockspec: -------------------------------------------------------------------------------- 1 | package = 'lp' 2 | version = '1.0-3' 3 | source = { 4 | url = 'git+https://github.com/dr-co/lp', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = "Event server Tarantool", 9 | homepage = 'https://github.com/dr-co/lp', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1' 14 | } 15 | build = { 16 | type = 'builtin', 17 | 18 | modules = { 19 | ['lp'] = 'lp/init.lua', 20 | ['lp.migrations'] = 'lp/migrations.lua', 21 | ['lp.pack_key'] = 'lp/pack_key.lua', 22 | } 23 | } 24 | 25 | -- vim: syntax=lua 26 | 27 | -------------------------------------------------------------------------------- /lp.rockspec.in: -------------------------------------------------------------------------------- 1 | package = 'lp' 2 | version = '@@VERSION@@' 3 | source = { 4 | url = 'git+https://github.com/dr-co/lp', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = "Event server Tarantool", 9 | homepage = 'https://github.com/dr-co/lp', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1' 14 | } 15 | build = { 16 | type = 'builtin', 17 | 18 | modules = { 19 | ['lp'] = 'lp/init.lua', 20 | ['lp.migrations'] = 'lp/migrations.lua', 21 | ['lp.pack_key'] = 'lp/pack_key.lua', 22 | } 23 | } 24 | 25 | -- vim: syntax=lua 26 | 27 | -------------------------------------------------------------------------------- /lp/init.lua: -------------------------------------------------------------------------------- 1 | local ID = 1 2 | local CREATED = 2 3 | local KEY = 3 4 | local DATA = 4 5 | 6 | local log = require 'log' 7 | local fiber = require 'fiber' 8 | local pack_key = require 'lp.pack_key' 9 | local msgpack = require 'msgpack' 10 | 11 | local lp = { 12 | VERSION = '1.0-3', 13 | _is_lp = true, 14 | 15 | defaults = { 16 | expire_timeout = 1800, 17 | serialize_key_method = 'none', 18 | lsn_check_interval = 1, 19 | mode = 'master', 20 | }, 21 | 22 | private = { 23 | migrations = require('lp.migrations'), 24 | waiter = {}, 25 | 26 | cond_run = { instance = true, fiber = {} }, 27 | 28 | first_id = nil, 29 | last_id = nil, 30 | processed_id = nil, 31 | }, 32 | } 33 | 34 | lp.private._pack = pack_key.none.pack 35 | lp.private._unpack = pack_key.none.unpack 36 | 37 | function lp:_extend(t1, t2) 38 | local res = {} 39 | if t1 ~= nil then 40 | for k, v in pairs(t1) do 41 | res[k] = v 42 | end 43 | end 44 | 45 | if t2 ~= nil then 46 | for k, v in pairs(t2) do 47 | if res[k] ~= nil and v ~= nil and type(res[k]) ~= type(v) then 48 | box.error(box.error.PROC_LUA, 49 | string.format( 50 | 'Wrong type for ".%s": %s (have to be %s)', 51 | tostring(k), 52 | type(v), 53 | type(res[k]) 54 | ) 55 | ) 56 | end 57 | res[k] = v 58 | end 59 | end 60 | return res 61 | end 62 | 63 | function lp:_last_id() 64 | local max = box.space.LP.index.id:max() 65 | 66 | local res 67 | if max ~= nil then 68 | res = max[ID] 69 | else 70 | res = tonumber64(0) 71 | end 72 | 73 | if self.private.last_id == nil then 74 | self.private.last_id = res 75 | end 76 | 77 | if self.private.last_id < res then 78 | self.private.last_id = res 79 | end 80 | 81 | return self.private.last_id 82 | end 83 | 84 | function lp:_first_id() 85 | local min = box.space.LP.index.id:min() 86 | if min == nil then 87 | if self.private.first_id == nil then 88 | self.private.first_id = 0 89 | end 90 | else 91 | self.private.first_id = min[ID] 92 | end 93 | return self.private.first_id 94 | end 95 | 96 | 97 | 98 | function lp:_take(id, keys) 99 | id = tonumber64(id) 100 | local res = {} 101 | 102 | for i, key in pairs(keys) do 103 | for _, tuple in box.space.LP.index.subscribe:pairs({ key, id }, { iterator ='GE' }) do 104 | if tuple[KEY] ~= key then 105 | break 106 | end 107 | table.insert(res, tuple) 108 | end 109 | end 110 | table.sort(res, function(a, b) return a[1] < b[1] end) 111 | local result = {} 112 | for _, tuple in pairs(res) do 113 | table.insert(result, 114 | tuple:transform(KEY, 1, self.private._unpack(tuple[KEY]))) 115 | end 116 | return result 117 | end 118 | 119 | function lp:_wakeup_consumers(key) 120 | if not self.private.waiter[key] then 121 | return 122 | end 123 | 124 | for fid, keys in pairs(self.private.waiter[key]) do 125 | if keys then 126 | for _, k in pairs(keys) do 127 | self.private.waiter[k][fid] = nil 128 | end 129 | fiber.find(fid):wakeup() 130 | end 131 | end 132 | end 133 | 134 | 135 | function lp:_lsn_fiber() 136 | local cond = self.private.cond_run 137 | fiber.create(function() 138 | fiber.name('LP-lsn') 139 | self.private.processed_id = self:_last_id() 140 | 141 | log.info( 142 | 'LP: fiber `lsn` started (last_id = %s)', 143 | self.private.processed_id 144 | ) 145 | 146 | local pause = self.opts.lsn_check_interval 147 | 148 | local json = require 'json' 149 | while cond.instance do 150 | local lsn = tonumber64(0) 151 | for _, lsn_s in pairs(box.info.vclock) do 152 | lsn = lsn + tonumber64(lsn_s) 153 | end 154 | if self.opts.mode ~= 'master' then 155 | -- update last_id 156 | self:_last_id() 157 | end 158 | 159 | if self.private.lsn ~= lsn then 160 | self.private.lsn = lsn 161 | 162 | while true do 163 | local task = box.space.LP:get( 164 | self.private.processed_id + tonumber64(1) 165 | ) 166 | if task then 167 | self.private.processed_id = task[ID] 168 | self:_wakeup_consumers(task[KEY]) 169 | else 170 | if self.private.processed_id == self:_last_id() then 171 | break 172 | end 173 | self.private.processed_id = 174 | self.private.processed_id + tonumber64(1) 175 | end 176 | end 177 | end 178 | 179 | cond.fiber.lsn = fiber.id() 180 | fiber.sleep(pause) 181 | if cond.fiber.lsn then 182 | pause = self.opts.lsn_check_interval 183 | else 184 | -- someone woke up us 185 | -- on_replace - is BEFORE trigger 186 | pause = 0.05 187 | if pause > self.opts.lsn_check_interval then 188 | pause = self.opts.lsn_check_interval 189 | end 190 | end 191 | cond.fiber.lsn = nil 192 | end 193 | end) 194 | end 195 | 196 | function lp:_expire_fiber() 197 | if self.opts.mode ~= 'master' then 198 | log.info('LP: expired is disabled: mode=%s', self.opts.mode) 199 | return 200 | end 201 | 202 | local cond = self.private.cond_run 203 | fiber.create(function() 204 | fiber.name('LP-expired') 205 | log.info('LP: fiber `expired` started') 206 | while cond.instance do 207 | local pause 208 | local task = box.space.LP.index.id:min() 209 | if task ~= nil then 210 | pause = fiber.time() - task[CREATED] 211 | pause = pause - self.opts.expire_timeout 212 | if pause >= 0 then 213 | self.private.first_id = tonumber64(task[ID]) + tonumber64(1) 214 | box.space.LP:delete(task[ID]) 215 | pause = nil 216 | else 217 | pause = -pause 218 | end 219 | else 220 | pause = self.opts.expire_timeout 221 | end 222 | if pause then 223 | fiber.sleep(pause) 224 | end 225 | end 226 | end) 227 | end 228 | 229 | ------------------------------------------------------------------------------- 230 | -- Public API 231 | ------------------------------------------------------------------------------- 232 | 233 | function lp:subscribe(id, timeout, ...) 234 | 235 | if not ('table' == type(self) and self._is_lp) then 236 | box.error(box.error.PROC_LUA, 'usage lp:subscribe(id, timeout, key1[, ... ]') 237 | end 238 | 239 | local pkeys = {...} 240 | 241 | local keys = {} 242 | for i, k in pairs(pkeys) do 243 | table.insert(keys, self.private._pack(k)) 244 | end 245 | 246 | id = tonumber64(id) 247 | 248 | -- Инициализация: если 0 - сразу возвращаем ID/MinID 249 | -- Если id потерялся, то сразу отвечаем проблемой 250 | if id == tonumber64(0) or id < self:_first_id() then 251 | return { 252 | box.tuple.new{ self:_last_id() + tonumber64(1), self:_first_id() } 253 | } 254 | end 255 | 256 | local events = self:_take(id, keys) 257 | 258 | if #events > 0 then 259 | table.insert( 260 | events, 261 | box.tuple.new{ self:_last_id() + tonumber64(1), self:_first_id() } 262 | ) 263 | return events 264 | end 265 | 266 | timeout = tonumber(timeout) 267 | local started 268 | local fid = fiber.id() 269 | 270 | while timeout > 0 do 271 | started = fiber.time() 272 | 273 | -- set waiter fid 274 | for _, key in pairs(keys) do 275 | if self.private.waiter[key] == nil then 276 | self.private.waiter[key] = {} 277 | end 278 | self.private.waiter[key][fid] = keys 279 | end 280 | 281 | fiber.sleep(timeout) 282 | 283 | for _, key in pairs(keys) do 284 | self.private.waiter[key][fid] = nil 285 | end 286 | 287 | timeout = timeout - (fiber.time() - started) 288 | 289 | events = self:_take(id, keys) 290 | if #events > 0 then 291 | break 292 | end 293 | end 294 | 295 | local last_id = self:_last_id() 296 | if id <= last_id then 297 | id = last_id + tonumber64(1) 298 | end 299 | 300 | table.insert( 301 | events, 302 | box.tuple.new{ id, self:_first_id() } 303 | ) 304 | return events 305 | end 306 | 307 | function lp:push(key, data) 308 | if not ('table' == type(self) and self._is_lp) then 309 | box.error(box.error.PROC_LUA, 'usage lp:push(key[, data])') 310 | end 311 | 312 | key = self.private._pack(key) 313 | local time = fiber.time() 314 | if data == nil then 315 | data = msgpack.NULL 316 | end 317 | local task = box.space.LP:insert{ 318 | self:_last_id() + 1, 319 | fiber.time(), 320 | key, 321 | data 322 | } 323 | return task:transform(KEY, 1, self.private._unpack(task[KEY])) 324 | end 325 | 326 | function lp:push_list(...) 327 | 328 | if not ('table' == type(self) and self._is_lp) then 329 | box.error(box.error.PROC_LUA, 'usage lp:push_list(key[, data][,...])') 330 | end 331 | 332 | local put = {...} 333 | local i = 1 334 | local count = 0 335 | 336 | box.begin() 337 | while i <= #put do 338 | local key = put[ i ] 339 | local data = put[ i + 1 ] 340 | i = i + 2 341 | count = count + 1 342 | self:push(key, data) 343 | end 344 | box.commit() 345 | 346 | return count 347 | end 348 | 349 | function lp:init(defaults) 350 | 351 | if not ('table' == type(self) and self._is_lp) then 352 | box.error(box.error.PROC_LUA, 'usage lp:init{...}') 353 | end 354 | 355 | local opts = self:_extend(self.defaults, defaults) 356 | 357 | self.opts = opts 358 | local upgrades = 0 359 | 360 | if self.opts.mode == 'master' then 361 | upgrades = self.private.migrations:upgrade(self) 362 | else 363 | while not box.space.LP do 364 | log.info('Wait for master process will create space') 365 | fiber.sleep(2) 366 | end 367 | end 368 | log.info('LP (%s) started', self.opts.mode) 369 | 370 | for _, trg in pairs(box.space.LP:on_replace()) do 371 | box.space.LP:on_replace(nil, trg) 372 | end 373 | 374 | -- stop old fibers 375 | self.private.cond_run.instance = false 376 | for _, fid in pairs(self.private.cond_run.fiber) do 377 | fiber.find(fid):wakeup() 378 | end 379 | 380 | 381 | if pack_key[self.opts.serialize_key_method] == nil then 382 | self.opts.serialize_key_method = 'none' 383 | end 384 | log.info('LP: use "%s" method to pack key', self.opts.serialize_key_method) 385 | self.private._pack = pack_key[self.opts.serialize_key_method].pack 386 | self.private._unpack = pack_key[self.opts.serialize_key_method].unpack 387 | 388 | 389 | -- condition for new fibers 390 | self.private.cond_run = { instance = true, fiber = {} } 391 | 392 | -- start lsn fiber 393 | self:_lsn_fiber() 394 | 395 | box.space.LP:on_replace(function(old, new) 396 | local fid = self.private.cond_run.fiber.lsn 397 | 398 | -- delete tuple 399 | if old ~= nil and new == nil then 400 | self.private.first_id = old[ID] + tonumber64(1) 401 | end 402 | if fid then 403 | self.private.cond_run.fiber.lsn = nil 404 | fiber.find(fid):wakeup() 405 | end 406 | end) 407 | 408 | -- start expire fiber 409 | self:_expire_fiber() 410 | 411 | for _, key in pairs(self.private.waiter) do 412 | self:_wakeup_consumers(key) 413 | end 414 | 415 | return upgrades 416 | end 417 | 418 | local private = {} 419 | local public = {} 420 | 421 | for k, v in pairs(lp) do 422 | if string.match(k, '^_') then 423 | private[k] = v 424 | else 425 | public[k] = v 426 | end 427 | end 428 | 429 | local call_methods = { 430 | push = true, 431 | push_list = true, 432 | subscribe = true, 433 | init = true 434 | } 435 | 436 | setmetatable(public, { 437 | __index = private, 438 | __call = function(self, method, ...) 439 | if call_methods[method] then 440 | return self[method](self, ...) 441 | end 442 | box.error(box.error.PROC_LUA, 'usage: lp(method, args...)') 443 | end 444 | }) 445 | 446 | return public 447 | 448 | -------------------------------------------------------------------------------- /lp/migrations.lua: -------------------------------------------------------------------------------- 1 | local SCH_KEY = 'drco_LP' 2 | 3 | local log = require 'log' 4 | local migrations = {} 5 | migrations.list = { 6 | { 7 | up = function() 8 | log.info('First start of LP detected') 9 | end 10 | }, 11 | { 12 | description = 'Create main LP space', 13 | up = function() 14 | box.schema.space.create( 15 | 'LP', 16 | { 17 | engine = 'memtx', 18 | format = { 19 | { -- #1 20 | ['name'] = 'id', 21 | ['type'] = 'unsigned', 22 | }, 23 | 24 | { -- #2 25 | ['name'] = 'created', 26 | ['type'] = 'number', 27 | }, 28 | 29 | { -- #3 30 | ['name'] = 'key', 31 | ['type'] = 'str', 32 | }, 33 | 34 | { -- #4 35 | ['name'] = 'data', 36 | ['type'] = '*', 37 | }, 38 | } 39 | } 40 | ) 41 | end 42 | }, 43 | 44 | { 45 | description = 'Create primary LP index', 46 | up = function() 47 | box.space.LP:create_index( 48 | 'id', 49 | { 50 | unique = true, 51 | type = 'tree', 52 | parts = { 1, 'unsigned' } 53 | } 54 | ) 55 | end 56 | }, 57 | 58 | { 59 | description = 'Create fetch LP index', 60 | up = function() 61 | box.space.LP:create_index( 62 | 'subscribe', 63 | { 64 | unique = false, 65 | type = 'tree', 66 | parts = { 3, 'str', 1, 'unsigned' } 67 | } 68 | ) 69 | end 70 | } 71 | } 72 | 73 | 74 | function migrations.upgrade(self, mq) 75 | 76 | local db_version = 0 77 | local ut = box.space._schema:get(SCH_KEY) 78 | local version = mq.VERSION 79 | 80 | if ut ~= nil then 81 | db_version = ut[2] 82 | end 83 | 84 | local cnt = 0 85 | for v, m in pairs(migrations.list) do 86 | if db_version < v then 87 | local nv = string.format('%s.%03d', version, v) 88 | log.info('LP: up to version %s (%s)', nv, m.description) 89 | m.up(mq) 90 | box.space._schema:replace{ SCH_KEY, v } 91 | mq.VERSION = nv 92 | cnt = cnt + 1 93 | end 94 | end 95 | return cnt 96 | end 97 | 98 | 99 | return migrations 100 | 101 | -------------------------------------------------------------------------------- /lp/pack_key.lua: -------------------------------------------------------------------------------- 1 | local msgpack = require 'msgpack' 2 | local json = require 'json' 3 | 4 | return { 5 | none = { 6 | pack = function(key) 7 | return key 8 | end, 9 | 10 | unpack = function(pkey) 11 | return pkey 12 | end 13 | }, 14 | msgpack = { 15 | pack = function(key) 16 | return msgpack.encode(key) 17 | end, 18 | 19 | unpack = function(pkey) 20 | local key = msgpack.decode(pkey) 21 | return key 22 | end 23 | }, 24 | json = { 25 | pack = function(key) 26 | return json.encode(key) 27 | end, 28 | 29 | unpack = function(pkey) 30 | return json.decode(pkey) 31 | end 32 | }, 33 | 34 | colon = { 35 | pack = function(key) 36 | if type(key) == 'table' then 37 | local res = {} 38 | for _, v in pairs(key) do 39 | if v == nil then 40 | table.insert(res, 'null') 41 | else 42 | table.insert(res, tostring(v)) 43 | end 44 | end 45 | return table.concat(res, ':') 46 | end 47 | return key .. ':' 48 | end, 49 | unpack = function(pkey) 50 | if string.match(pkey, ':$') then 51 | return string.sub(pkey, 1, string.len(pkey) - 1) 52 | end 53 | 54 | local res = {} 55 | while true do 56 | local pos = string.find(pkey, ':') 57 | if pos == nil then 58 | table.insert(res, pkey) 59 | return res 60 | end 61 | table.insert(res, string.sub(pkey, 1, pos - 1)) 62 | pkey = string.sub(pkey, pos + 1) 63 | end 64 | end 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /t/010-base.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local test = require('tap').test() 4 | test:plan(1) 5 | 6 | local tnt = require('t.tnt') 7 | test:ok(tnt, 'tarantool loaded') 8 | tnt.cfg{} 9 | 10 | tnt.finish() 11 | os.exit(test:check() == true and 0 or -1) 12 | -------------------------------------------------------------------------------- /t/015-pack_key.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local test = require('tap').test() 4 | test:plan(4) 5 | 6 | package.path = string.format('%s;%s', 7 | 'lua/?.lua;lua/?/init.lua', 8 | package.path 9 | ) 10 | local msgpack = require 'msgpack' 11 | 12 | local pk = require 'lp.pack_key' 13 | 14 | 15 | function check_method(m) 16 | test:test(m .. ' method', function(test) 17 | test:plan(5) 18 | 19 | test:ok(pk[m].pack('abc'), m .. ': pack scalar') 20 | test:ok(pk[m].pack({'abc'}), m .. ': pack table') 21 | test:is_deeply( 22 | pk[m].unpack(pk[m].pack('abc')), 23 | 'abc', 24 | m .. ': pack/unpack table' 25 | ) 26 | 27 | test:ok(pk[m].pack{ 'abc', msgpack.NULL }, 'pack null') 28 | if m == 'colon' then 29 | test:diag(pk[m].pack{'abc', msgpack.NULL}) 30 | test:is_deeply( 31 | pk[m].unpack(pk[m].pack{'abc', msgpack.NULL}), 32 | { 'abc', 'null' }, 33 | m .. ': pack/unpack table' 34 | ) 35 | else 36 | test:is_deeply( 37 | pk[m].unpack(pk[m].pack{'abc', msgpack.NULL}), 38 | { 'abc', msgpack.NULL }, 39 | m .. ': pack/unpack table' 40 | ) 41 | end 42 | end) 43 | end 44 | 45 | check_method 'msgpack' 46 | check_method 'json' 47 | check_method 'none' 48 | check_method 'colon' 49 | 50 | 51 | os.exit(test:check() == true and 0 or -1) 52 | 53 | 54 | -------------------------------------------------------------------------------- /t/020-require.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local yaml = require 'yaml' 4 | local test = require('tap').test() 5 | test:plan(5) 6 | 7 | local tnt = require('t.tnt') 8 | test:ok(tnt, 'tarantool loaded') 9 | tnt.cfg{} 10 | 11 | package.path = string.format('%s;%s', 12 | 'lua/?.lua;lua/?/init.lua', 13 | package.path 14 | ) 15 | 16 | local lp = require 'lp' 17 | 18 | test:ok(lp:init() > 0, 'First init lp') 19 | test:like(tnt.log(), 'First start of LP', 'upgrade process started') 20 | test:is(lp:init(), 0, 'Reinit does nothing') 21 | test:ok(box.space.LP, 'Space created') 22 | 23 | 24 | tnt.finish() 25 | os.exit(test:check() == true and 0 or -1) 26 | 27 | -------------------------------------------------------------------------------- /t/030-put.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local yaml = require 'yaml' 4 | local msgpack = require 'msgpack' 5 | local test = require('tap').test() 6 | test:plan(11) 7 | 8 | local tnt = require('t.tnt') 9 | test:ok(tnt, 'tarantool loaded') 10 | tnt.cfg{} 11 | 12 | package.path = string.format('%s;%s', 13 | 'lua/?.lua;lua/?/init.lua', 14 | package.path 15 | ) 16 | 17 | local lp = require 'lp' 18 | test:ok(lp:init(), 'LP init done') 19 | 20 | local task = lp:push('key', 'value') 21 | test:ok(task, 'task was put') 22 | 23 | local task2 = lp:push('key', 'value') 24 | test:ok(task2, 'task was put again') 25 | 26 | test:is(task2[1], task[1] + 1, 'autoincrement id') 27 | 28 | local task3 = lp:push('key', msgpack.null) 29 | test:ok(task3, 'task with no data was put') 30 | test:is(task3[1], task2[1] + 1, 'autoincrement id') 31 | test:ok(task3[4] == nil, 'data is nil') 32 | 33 | local cnt = lp:push_list('key1', 'data1', 'key2', 'data2') 34 | test:is(cnt, 2, 'tuples inserted') 35 | 36 | task = lp('push', 'a', 'b') 37 | test:ok(task, 'call-style push') 38 | 39 | count = lp('push_list', 'a', 'b', 'c', 'd', 'e') 40 | test:is(count, 3, 'push_list') 41 | 42 | tnt.finish() 43 | os.exit(test:check() == true and 0 or -1) 44 | 45 | -------------------------------------------------------------------------------- /t/040-take.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local yaml = require 'yaml' 4 | local test = require('tap').test() 5 | local fiber = require 'fiber' 6 | test:plan(8) 7 | 8 | local tnt = require('t.tnt') 9 | test:ok(tnt, 'tarantool loaded') 10 | tnt.cfg{} 11 | 12 | package.path = string.format('%s;%s', 13 | 'lua/?.lua;lua/?/init.lua', 14 | package.path 15 | ) 16 | 17 | local lp = require 'lp' 18 | test:ok(lp:init(), 'LP init done') 19 | 20 | local task = lp:push('key', 'value') 21 | test:ok(task, 'task was put') 22 | 23 | 24 | test:test('take immediatelly', function(test) 25 | test:plan(3) 26 | local list = lp:subscribe(1, 0, 'key') 27 | test:is(#list, 2, 'one event was fetch immediattely') 28 | test:is(list[1][4], 'value', 'event data') 29 | 30 | test:is(list[#list][1], list[1][1] + 1, 'next id') 31 | end) 32 | 33 | 34 | test:test('take timeout', function(test) 35 | test:plan(2) 36 | 37 | local started = fiber.time() 38 | 39 | local list = lp:subscribe(1, 0.1, 'key1') 40 | test:is(#list, 1, 'no events are fetched') 41 | test:ok(fiber.time() - started >= 0.1, 'timeout reached') 42 | end) 43 | 44 | 45 | test:test('some keys', function(test) 46 | box.space.LP:truncate() 47 | 48 | test:plan(3) 49 | test:is( 50 | lp:push_list( 51 | 'key1', 'value1', 52 | 'key2', 'value2', 53 | 'key3', 'value3' 54 | ), 55 | 3, 56 | '3 tasks were put') 57 | 58 | local list = lp:subscribe(1, 0.1, 'key2') 59 | test:is(#list, 1, 'no one event was fetched') 60 | 61 | list = lp:subscribe(list[1][2], 0.1, 'key2') 62 | test:is(#list, 2, 'one event was fetched') 63 | end) 64 | 65 | test:test('take after sleep', function(test) 66 | 67 | test:plan(12) 68 | 69 | test:diag(lp:_last_id()) 70 | 71 | local count = 0 72 | fiber.create(function() 73 | count = count + 1 74 | local list = lp:subscribe(lp:_last_id(), 2, 'a', 'b', 'c') 75 | test:is(#list, 2, 'event received') 76 | count = count + 10 77 | test:is(list[1][3], 'a', 'key') 78 | test:is(list[1][4], 'da', 'data') 79 | end) 80 | 81 | fiber.create(function() 82 | count = count + 1 83 | local list = lp:subscribe(lp:_last_id(), 2, 'd', 'e', 'f') 84 | test:is(#list, 2, 'event received') 85 | count = count + 10 86 | test:is(list[1][3], 'e', 'key') 87 | test:is(list[1][4], 'de', 'data') 88 | end) 89 | 90 | fiber.create(function() 91 | count = count + 1 92 | local list = lp:subscribe(lp:_last_id(), 2, 'g', 'h', 'i') 93 | test:is(#list, 2, 'event received') 94 | count = count + 10 95 | test:is(list[1][3], 'i', 'key') 96 | test:is(list[1][4], 'di', 'data') 97 | end) 98 | 99 | 100 | local started = fiber.time() 101 | fiber.sleep(0.2) 102 | test:is(count, 3, 'all fibers started') 103 | lp:push('a', 'da') 104 | lp:push('e', 'de') 105 | lp:push('i', 'di') 106 | 107 | for i = 1, 10 do 108 | if count == 33 then 109 | break 110 | end 111 | 112 | fiber.sleep(0.09) 113 | end 114 | test:ok(fiber.time() - started >= 0.2, 'time lo') 115 | test:ok(fiber.time() - started <= 0.5, 'time hi') 116 | 117 | end) 118 | 119 | 120 | test:test('id > _last_id', function(test) 121 | test:plan(7) 122 | 123 | local id = lp:_last_id() + tonumber(10) 124 | 125 | local list = lp:subscribe(id, 0.1, 'a', 'b', 'c') 126 | test:is(#list, 1, 'no tasks were taken') 127 | test:is(list[1][1], id, 'id was not changed') 128 | 129 | 130 | id = lp:_last_id() + tonumber(1) 131 | 132 | local fiber_run = false 133 | local fiber_done = false 134 | fiber.create(function() 135 | fiber_run = true 136 | local list = lp:subscribe(id, 2, 'a', 'b', 'c') 137 | test:is(#list, 2, 'one task was taken') 138 | test:is(list[1][1], id, 'id') 139 | test:is(list[1][4], 'd', 'data') 140 | fiber_done = true 141 | end) 142 | 143 | fiber.sleep(0.1) 144 | test:ok(fiber_run, 'fiber is run') 145 | lp:push('c', 'd') 146 | for i = 1, 10 do 147 | fiber.sleep(0.1) 148 | if fiber_done then 149 | break 150 | end 151 | end 152 | test:ok(fiber_done, 'fiber is done') 153 | 154 | end) 155 | 156 | -- test:diag(yaml.encode(box.space.LP:select())) 157 | -- test:diag(tnt.log()) 158 | tnt.finish() 159 | os.exit(test:check() == true and 0 or -1) 160 | 161 | -------------------------------------------------------------------------------- /t/050-push-subscribe.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local yaml = require 'yaml' 4 | local test = require('tap').test() 5 | local fiber = require 'fiber' 6 | test:plan(3) 7 | 8 | local tnt = require('t.tnt') 9 | test:ok(tnt, 'tarantool loaded') 10 | tnt.cfg{} 11 | 12 | package.path = string.format('%s;%s', 13 | 'lua/?.lua;lua/?/init.lua', 14 | package.path 15 | ) 16 | 17 | local lp = require 'lp' 18 | test:ok(lp:init(), 'LP init done') 19 | 20 | fiber.create(function() 21 | fiber.sleep(0.2) 22 | lp:push('key', 'value') 23 | end) 24 | 25 | 26 | test:test('take sleep', function(test) 27 | test:plan(5) 28 | local started = fiber.time() 29 | local list = lp:subscribe(1, 1, 'key') 30 | test:ok(fiber.time() - started >= 0.15, 'delay lo') 31 | test:ok(fiber.time() - started <= 0.35, 'delay hi') 32 | test:is(#list, 2, 'one event was fetch immediattely') 33 | test:is(list[1][4], 'value', 'event data') 34 | 35 | test:is(list[#list][1], list[1][1] + 1, 'next id') 36 | end) 37 | 38 | 39 | -- test:diag(tnt.log()) 40 | tnt.finish() 41 | os.exit(test:check() == true and 0 or -1) 42 | 43 | -------------------------------------------------------------------------------- /t/060-expired.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local yaml = require 'yaml' 4 | local test = require('tap').test() 5 | local fiber = require 'fiber' 6 | test:plan(5) 7 | 8 | local tnt = require('t.tnt') 9 | test:ok(tnt, 'tarantool loaded') 10 | tnt.cfg{} 11 | 12 | package.path = string.format('%s;%s', 13 | 'lua/?.lua;lua/?/init.lua', 14 | package.path 15 | ) 16 | 17 | local lp = require 'lp' 18 | test:ok(lp:init({ expire_timeout = 0.2 }), 'LP init done') 19 | 20 | test:ok(lp:push('key', 'value'), 'push task') 21 | 22 | 23 | test:test('take immediatelly', function(test) 24 | test:plan(3) 25 | local list = lp:subscribe(1, 1, 'key') 26 | test:is(#list, 2, 'one event was fetch immediattely') 27 | test:is(list[1][4], 'value', 'event data') 28 | 29 | test:is(list[#list][1], list[1][1] + 1, 'next id') 30 | end) 31 | 32 | fiber.sleep(0.21) 33 | 34 | test:test('expire process removed task', function(test) 35 | test:plan(3) 36 | local list = lp:subscribe(1, 1, 'key') 37 | test:is(#list, 1, 'no one event was fetch') 38 | test:is(list[1][1], list[1][2], 'database empty') 39 | test:ok(list[1][1] > 1, 'id more than one') 40 | end) 41 | 42 | -- test:diag(tnt.log()) 43 | tnt.finish() 44 | os.exit(test:check() == true and 0 or -1) 45 | 46 | -------------------------------------------------------------------------------- /t/100-master-slave.master.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local PLAN = 18 4 | 5 | local MASTER_PORT = tonumber(os.getenv('MASTER_PORT')) 6 | local REPLICA_PORT = tonumber(os.getenv('REPLICA_PORT')) 7 | local TEST_DIR = os.getenv('TEST_DIR') 8 | 9 | local fio = require 'fio' 10 | local log = require 'log' 11 | local dir = fio.pathjoin(TEST_DIR, 'master') 12 | 13 | fio.mkdir(dir) 14 | 15 | box.cfg { 16 | listen = MASTER_PORT, 17 | 18 | wal_dir = dir, 19 | memtx_dir = dir, 20 | vinyl_dir = dir, 21 | 22 | log = fio.pathjoin(dir, 'tarantool.log'), 23 | } 24 | 25 | box.schema.user.create('test', { password = 'test' }) 26 | box.schema.user.grant('test', 'read,write,execute', 'universe') 27 | 28 | package.path = string.format('%s;%s', 29 | 'lua/?.lua;lua/?/init.lua', 30 | package.path 31 | ) 32 | 33 | ------------------------------------------------------------------------------- 34 | -- TEST 35 | ------------------------------------------------------------------------------- 36 | 37 | local fiber = require 'fiber' 38 | local net = require 'net.box' 39 | local yaml = require 'yaml' 40 | 41 | local test = require('tap').test() 42 | test:plan(PLAN) 43 | 44 | 45 | 46 | local lp = require 'lp' 47 | 48 | test:ok(lp:init({ mode = 'master', expire_timeout = 0.5 }) > 0, 'First init lp') 49 | test:ok(box.space.LP, 'Space created') 50 | 51 | 52 | local replica 53 | 54 | for i = 1, 10 do 55 | replica = net.connect(string.format('%s:%s@localhost:%s', 'test', 'test', REPLICA_PORT)) 56 | if replica:ping() then 57 | break 58 | end 59 | fiber.sleep(0.25) 60 | end 61 | 62 | test:ok(replica:ping(), 'replica is up') 63 | 64 | 65 | log.info('push task1') 66 | test:ok(lp:push('key1', 'value1'), 'push 1 task') 67 | 68 | log.info('push task2') 69 | local task = lp:push('key2', 'value2') 70 | test:ok(task, 'push 2 task') 71 | 72 | log.info('push task3') 73 | test:ok(lp:push('key3', 'value3'), 'push 3 task') 74 | 75 | local started = fiber.time() 76 | list = replica:call('lp:subscribe', { 1, 3, 'key2' }) 77 | test:is(#list, 2, 'one task received from replica') 78 | test:is(list[1][4], 'value2', 'task data') 79 | test:ok(fiber.time() - started < 0.1, 'wakeup fiber by trigger on_replace') 80 | 81 | 82 | fiber.sleep(0.61) 83 | 84 | local list2 = replica:call('lp:subscribe', { 1, 0.1, 'key2' }) 85 | test:is(#list2, 1, 'expired removed task') 86 | 87 | test:is(list2[1][1], list2[1][2], 'min_id = last_id') 88 | 89 | test:is_deeply(box.space.LP:select(), {}, 'empty space') 90 | 91 | 92 | 93 | test:is(lp:init({ mode = 'master', expire_timeout = 500 }), 0, 'Reinit') 94 | 95 | test:is(lp:push_list('key1', 'value1', 'key2', 'value2', 'key3', 'value3'), 3, 96 | '3 tasks were put') 97 | started = fiber.time() 98 | 99 | local list3 = replica:call('lp:subscribe', { list2[#list2][2], .1, 'key2' }) 100 | 101 | test:is(#list3, 2, 'one task received from replica') 102 | test:is(list3[1][4], 'value2', 'task data') 103 | test:ok(fiber.time() - started < 0.1, 'wakeup fiber by trigger on_replace') 104 | test:is(list3[1][1], list2[1][1] + 1, 'id') 105 | 106 | os.exit(test:check() == true and 0 or -1) 107 | 108 | -------------------------------------------------------------------------------- /t/100-master-slave.replica.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local MASTER_PORT = tonumber(os.getenv('MASTER_PORT')) 4 | local REPLICA_PORT = tonumber(os.getenv('REPLICA_PORT')) 5 | local TEST_DIR = os.getenv('TEST_DIR') 6 | 7 | local fio = require 'fio' 8 | 9 | local dir = fio.pathjoin(TEST_DIR, 'replica') 10 | 11 | fio.mkdir(dir) 12 | 13 | box.cfg { 14 | listen = REPLICA_PORT, 15 | 16 | wal_dir = dir, 17 | memtx_dir = dir, 18 | vinyl_dir = dir, 19 | 20 | replication = { 21 | string.format('%s:%s@localhost:%s', 'test', 'test', MASTER_PORT) 22 | }, 23 | 24 | log = fio.pathjoin(dir, 'tarantool.log'), 25 | } 26 | 27 | 28 | 29 | package.path = string.format('%s;%s', 30 | 'lua/?.lua;lua/?/init.lua', 31 | package.path 32 | ) 33 | 34 | 35 | 36 | _G.lp = require 'lp' 37 | 38 | lp:init({ mode = 'replica', lsn_check_interval = .1 }) 39 | 40 | -------------------------------------------------------------------------------- /t/100-master-slave.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use utf8; 7 | use open qw(:std :utf8); 8 | use IO::Socket::INET; 9 | use File::Temp 'tempdir'; 10 | use File::Path 'rmtree'; 11 | 12 | sub free_port { 13 | for (1 .. 1000) { 14 | my $s = IO::Socket::INET->new( 15 | Listen => 1, 16 | # ReusePort => 1, 17 | ); 18 | next unless $s; 19 | my $port = $s->sockport; 20 | close $s; 21 | return $port; 22 | } 23 | return undef; 24 | } 25 | 26 | my $master = free_port; 27 | my $replica; 28 | 29 | do { 30 | $replica = free_port; 31 | } while $replica == $master; 32 | 33 | $ENV{TEST_DIR} = tempdir; 34 | $ENV{MASTER_PORT} = $master; 35 | $ENV{REPLICA_PORT} = $replica; 36 | 37 | my $master_lua = 't/100-master-slave.master.lua'; 38 | my $replica_lua = 't/100-master-slave.replica.lua'; 39 | 40 | my ($master_pid, $replica_pid); 41 | 42 | my $exit_code = 0; 43 | 44 | unless ($replica_pid = fork) { 45 | select undef, undef, undef, 0.3; 46 | exec tarantool => $replica_lua; 47 | } 48 | 49 | if ($master_pid = open my $out, '-|', tarantool => $master_lua) { 50 | 51 | while (<$out>) { 52 | print; 53 | } 54 | 55 | waitpid $master_pid, 0; 56 | 57 | $master_pid = undef; 58 | exit $? >> 8; 59 | } 60 | 61 | 62 | 63 | END { 64 | for ($replica_pid, $master_pid) { 65 | next unless $_; 66 | kill TERM => $_; 67 | waitpid $_, 0; 68 | $_ = undef; 69 | } 70 | 71 | for (qw(master replica)) { 72 | last; 73 | my $log = `cat $ENV{TEST_DIR}/$_/tarantool.log`; 74 | print "=============== $_ log file ====================\n"; 75 | print $log; 76 | } 77 | rmtree $ENV{TEST_DIR}; 78 | } 79 | 80 | exit $exit_code; 81 | -------------------------------------------------------------------------------- /t/tnt/init.lua: -------------------------------------------------------------------------------- 1 | local fio = require 'fio' 2 | local errno = require 'errno' 3 | local yaml = require 'yaml' 4 | local log = require 'log' 5 | 6 | local dir = os.getenv('QUEUE_TMP') 7 | local cleanup = false 8 | 9 | if dir == nil then 10 | dir = fio.tempdir() 11 | cleanup = true 12 | end 13 | 14 | local function tnt_prepare(cfg_args) 15 | cfg_args = cfg_args or {} 16 | local files = fio.glob(fio.pathjoin(dir, '*')) 17 | for _, file in pairs(files) do 18 | if fio.basename(file) ~= 'tarantool.log' then 19 | log.info("skip removing %s", file) 20 | fio.unlink(file) 21 | end 22 | end 23 | 24 | cfg_args['wal_dir'] = dir 25 | -- cfg_args['snap_dir'] = dir 26 | cfg_args['memtx_dir'] = dir 27 | cfg_args['vinyl_dir'] = dir 28 | cfg_args['log'] = fio.pathjoin(dir, 'tarantool.log') 29 | 30 | box.cfg (cfg_args) 31 | end 32 | 33 | return { 34 | finish = function(code) 35 | local files = fio.glob(fio.pathjoin(dir, '*')) 36 | for _, file in pairs(files) do 37 | if fio.basename(file) == 'tarantool.log' and not cleanup then 38 | log.info("skip removing %s", file) 39 | else 40 | log.info("remove %s", file) 41 | fio.unlink(file) 42 | end 43 | end 44 | if cleanup then 45 | log.info("rmdir %s", dir) 46 | fio.rmdir(dir) 47 | end 48 | end, 49 | 50 | dir = function() 51 | return dir 52 | end, 53 | 54 | cleanup = function() 55 | return cleanup 56 | end, 57 | 58 | logfile = function() 59 | return fio.pathjoin(dir, 'tarantool.log') 60 | end, 61 | 62 | log = function() 63 | local fh = fio.open(fio.pathjoin(dir, 'tarantool.log'), 'O_RDONLY') 64 | if fh == nil then 65 | box.error(box.error.PROC_LUA, errno.strerror()) 66 | end 67 | 68 | local data = fh:read(16384) 69 | fh:close() 70 | return data 71 | end, 72 | 73 | cfg = tnt_prepare 74 | } 75 | 76 | 77 | --------------------------------------------------------------------------------