├── .gitignore ├── example ├── imagine.lua ├── graphite.lua ├── expirationd.lua ├── Dockerfile ├── init-docker.lua ├── README.md ├── Makefile └── project.lua ├── README.md ├── LICENSE ├── make.sh └── lua ├── expirationd.lua ├── graphite.lua └── imagine.lua /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /example/imagine.lua: -------------------------------------------------------------------------------- 1 | ../lua/imagine.lua -------------------------------------------------------------------------------- /example/graphite.lua: -------------------------------------------------------------------------------- 1 | ../lua/graphite.lua -------------------------------------------------------------------------------- /example/expirationd.lua: -------------------------------------------------------------------------------- 1 | ../lua/expirationd.lua -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tarantool/tarantool:1.10.3 2 | COPY example/*.lua /opt/tarantool/ 3 | EXPOSE 3302 4 | CMD ["tarantool", "/opt/tarantool/init-docker.lua"] 5 | -------------------------------------------------------------------------------- /example/init-docker.lua: -------------------------------------------------------------------------------- 1 | require('console').listen(3302) 2 | 3 | box.cfg({ 4 | custom_proc_title = 'project', 5 | }) 6 | 7 | dofile('project.lua') 8 | 9 | box.schema.user.create('client', {password = 'client', if_not_exists = true}) 10 | box.schema.user.grant('client', 'execute', 'role', 'client_role', {if_not_exists = true}) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imagine.lua 2 | 3 | Tarantool framework. 4 | 5 | - Provides convenient setup of an access control. 6 | - Auto configures monitoring with Graphite. 7 | - Auto reload for lua. 8 | 9 | ## Usage 10 | 11 | To create ready to go version of imagine.lua and Grafana dashboard run: 12 | ```sh 13 | ./make.sh PROJECT_NAME 14 | ``` 15 | 16 | ## Example 17 | [example](example/) 18 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # project 2 | 3 | ### Step 1: run tarantool 4 | ```sh 5 | make 6 | make run 7 | ``` 8 | 9 | ### Step 2: connect 10 | ```sh 11 | telnet localhost 39032 12 | ``` 13 | 14 | ### Step 3: play 15 | ``` 16 | $ telnet localhost 39032 17 | Trying 127.0.0.1... 18 | Connected to localhost. 19 | Escape character is '^]'. 20 | Tarantool 1.10.3 (Lua console) 21 | type 'help' for interactive help 22 | 23 | key.create('abracadabra', 1,2,3,4) 24 | --- 25 | ... 26 | 27 | key.get('abracadabra') 28 | --- 29 | - - ['abracadabra', 1577270503, 1, 2, 3, 4] 30 | ... 31 | 32 | ``` 33 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | NAME=project 2 | PORT=39031 3 | ADMIN_PORT=39032 4 | DOCKERFILE_PATH=example/Dockerfile 5 | 6 | .PHONY: build clean run start stop list 7 | 8 | build: 9 | cd .. && docker build -t $(NAME) -f $(DOCKERFILE_PATH) . 10 | 11 | clean: 12 | docker rm -f $(NAME) 13 | docker rmi -f $(NAME) 14 | 15 | run: 16 | docker run --restart=always -d -p $(PORT):3301 -p $(ADMIN_PORT):3302 --name $(NAME) $(NAME) 17 | docker port `docker ps -aq -f name=$(NAME)` 18 | 19 | start: 20 | docker start $(NAME) 21 | 22 | stop: 23 | docker stop `docker ps -aq -f name=$(NAME)` 24 | 25 | list: 26 | docker images $(NAME) 27 | @echo 28 | docker ps -a -f name=$(NAME) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Mail.ru Group 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /example/project.lua: -------------------------------------------------------------------------------- 1 | local imagine = require('imagine') 2 | local log = require('log') 3 | 4 | 5 | local KEY_LIFETIME = 60 6 | 7 | local function create(key, ...) 8 | box.space.key:insert({key, os.time(), ...}) 9 | end 10 | 11 | local function delete(key) 12 | box.space.key:delete({key}) 13 | end 14 | 15 | local function get(key) 16 | return box.space.key:select({key}) 17 | end 18 | 19 | 20 | local function init() 21 | box.schema.create_space('key', {if_not_exists = true}) 22 | box.space.key:create_index('pk', {type = 'hash', parts = {1, 'str'}, if_not_exists = true}) 23 | 24 | require('expirationd').run_task( 25 | 'project_expiration_task', 26 | 'key', 27 | function (args, t) return t[2] + KEY_LIFETIME < os.time() end, 28 | function (space, args, t) delete(t[1]) end 29 | ) 30 | 31 | log.info('init ok') 32 | end 33 | 34 | imagine.init({ 35 | init_func = init, 36 | 37 | roles = { 38 | client_role = { 39 | table = 'key', 40 | funcs = { 41 | create = imagine.atomic(create), 42 | delete = imagine.atomic(delete), 43 | get = imagine.atomic(get), 44 | }, 45 | }, 46 | }, 47 | 48 | graphite = { 49 | prefix = 'project', 50 | ip = '127.0.0.1', 51 | port = 2003, 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -u -o pipefail 3 | IFS=$'\n\t' 4 | 5 | 6 | if [[ $# -ge 1 && "$1" =~ ^[-_a-z0-9]+$ ]]; then 7 | name="$1" 8 | else 9 | echo "USAGE: $(basename $0) PROJECT_NAME" >&2 10 | exit 1 11 | fi 12 | 13 | 14 | mkdir -p dist 15 | 16 | for f in "lua/imagine.lua" "grafana/dashboard.json"; do 17 | sed "s/__PROJECT__/$name/" $f > dist/$(basename $f) 18 | done 19 | 20 | for f in "lua/expirationd.lua" "lua/graphite.lua"; do 21 | cp $f dist/$(basename $f) 22 | done 23 | 24 | cat < dist/$name.lua 25 | local imagine = require('imagine') 26 | local log = require('log') 27 | 28 | local function TODO(...) 29 | 30 | end 31 | 32 | local function init() 33 | -- box.schema.create_space('TODO', {if_not_exists = true}) 34 | -- box.space.TODO:create_index('TODO', {type = 'hash', parts = {1, 'str'}, if_not_exists = true}) 35 | 36 | -- require('expirationd').run_task( 37 | -- 'TODO', 38 | -- 'TODO', 39 | -- function (args, t) return TODO end, 40 | -- function (space, args, t) TODO end 41 | -- ) 42 | 43 | log.info('init ok') 44 | end 45 | 46 | imagine.init({ 47 | init_func = init, 48 | 49 | roles = { 50 | client_role = { 51 | table = '$name', 52 | funcs = { 53 | todo = imagine.atomic(TODO), 54 | }, 55 | }, 56 | }, 57 | 58 | graphite = { 59 | prefix = '$name', 60 | ip = 'TODO', 61 | port = TODO, 62 | }, 63 | }) 64 | EOF 65 | 66 | echo 'Done. Saved to dist/' $'\n' 67 | ls -al dist/ 68 | -------------------------------------------------------------------------------- /lua/expirationd.lua: -------------------------------------------------------------------------------- 1 | -- ========================================================================= -- 2 | -- Tarantool/Box expiration daemon 3 | -- 4 | -- Daemon management functions: 5 | -- - expirationd.start -- start a new expiration task 6 | -- - expirationd.stats -- show task stats 7 | -- - expirationd.update -- update expirationd from disk 8 | -- - expirationd.kill -- kill a running task 9 | -- - expirationd.task -- get an existing task 10 | -- - expirationd.tasks -- get list with all tasks 11 | -- ========================================================================= -- 12 | 13 | -- ========================================================================= -- 14 | -- local support functions 15 | -- ========================================================================= -- 16 | 17 | local log = require('log') 18 | local fiber = require('fiber') 19 | local fun = require('fun') 20 | 21 | -- get fiber id function 22 | local function get_fiber_id(fiber) 23 | local fid = 0 24 | if fiber ~= nil and fiber:status() ~= "dead" then 25 | fid = fiber:id() 26 | end 27 | return fid 28 | end 29 | 30 | local task_list = {} 31 | 32 | local constants = { 33 | -- default value of number of tuples that 34 | -- will be checked by one iteration 35 | default_tuples_per_iteration = 1024, 36 | -- default value of time required for full 37 | -- index scan (in seconds) 38 | default_full_scan_time = 3600, 39 | -- maximal worker delay (seconds) 40 | max_delay = 1, 41 | -- check worker interval 42 | check_interval = 1, 43 | -- force expirationd, even if started on replica (false by default) 44 | force = false 45 | } 46 | 47 | -- ========================================================================= -- 48 | -- Expiration daemon global variables 49 | -- ========================================================================= -- 50 | 51 | -- main table 52 | 53 | -- ========================================================================= -- 54 | -- Task local functions 55 | -- ========================================================================= -- 56 | 57 | -- ------------------------------------------------------------------------- -- 58 | -- Task fibers 59 | -- ------------------------------------------------------------------------- -- 60 | 61 | local function construct_key(space_id, tuple) 62 | return fun.map( 63 | function(x) return tuple[x.fieldno] end, 64 | box.space[space_id].index[0].parts 65 | ):totable() 66 | end 67 | 68 | local function expiration_process(task, tuple) 69 | task.checked_tuples_count = task.checked_tuples_count + 1 70 | if task.is_tuple_expired(task.args, tuple) then 71 | task.expired_tuples_count = task.expired_tuples_count + 1 72 | task.process_expired_tuple(task.space_id, task.args, tuple) 73 | end 74 | end 75 | 76 | local function suspend(scan_space, task) 77 | if scan_space:len() > 0 then 78 | local delay = (task.tuples_per_iteration * task.full_scan_time) 79 | delay = delay / scan_space:len() 80 | if delay > constants.max_delay then 81 | delay = constants.max_delay 82 | end 83 | fiber.sleep(delay) 84 | end 85 | end 86 | 87 | local function tree_index_iter(scan_space, task) 88 | -- iteration with GT iterator 89 | local params = {iterator = 'GT', limit = task.tuples_per_iteration} 90 | local last_id 91 | local tuples = scan_space.index[0]:select({}, params) 92 | while #tuples > 0 do 93 | last_id = tuples[#tuples] 94 | for _, tuple in pairs(tuples) do 95 | expiration_process(task, tuple) 96 | end 97 | local key = construct_key(scan_space.id, last_id) 98 | tuples = scan_space.index[0]:select(key, params) 99 | suspend(scan_space, task) 100 | end 101 | end 102 | 103 | local function hash_index_iter(scan_space, task) 104 | -- iteration for hash index 105 | local checked_tuples_count = 0 106 | for _, tuple in scan_space.index[0]:pairs(nil, {iterator = box.index.ALL}) do 107 | checked_tuples_count = checked_tuples_count + 1 108 | expiration_process(task, tuple) 109 | -- find out if the worker can go to sleep 110 | if checked_tuples_count >= task.tuples_per_iteration then 111 | checked_tuples_count = 0 112 | suspend(scan_space, task) 113 | end 114 | end 115 | end 116 | 117 | local function do_worker_iteration(task) 118 | local scan_space = box.space[task.space_id] 119 | local index_type = scan_space.index[0].type 120 | 121 | -- full index scan loop 122 | if index_type == 'HASH' then 123 | hash_index_iter(scan_space, task) 124 | else 125 | tree_index_iter(scan_space, task) 126 | end 127 | end 128 | 129 | local function worker_loop(task) 130 | -- detach worker from the guardian and attach it to sched fiber 131 | fiber.self():name(task.name) 132 | 133 | while true do 134 | if box.cfg.replication_source == nil or task.force then 135 | do_worker_iteration(task) 136 | end 137 | 138 | -- iteration is complete, yield 139 | fiber.sleep(constants.max_delay) 140 | end 141 | end 142 | 143 | local function guardian_loop(task) 144 | -- detach the guardian from the creator and attach it to sched 145 | fiber.self():name(string.format("guardian of %q", task.name)) 146 | 147 | while true do 148 | if get_fiber_id(task.worker_fiber) == 0 then 149 | -- create worker fiber 150 | task.worker_fiber = fiber.create(worker_loop, task) 151 | 152 | log.info("expiration: task %q restarted", task.name) 153 | task.restarts = task.restarts + 1 154 | end 155 | fiber.sleep(constants.check_interval) 156 | end 157 | end 158 | 159 | -- ------------------------------------------------------------------------- -- 160 | -- Task management 161 | -- ------------------------------------------------------------------------- -- 162 | 163 | -- Task methods: 164 | -- * task:start() -- start task 165 | -- * task:stop() -- stop task 166 | -- * task:restart() -- restart task 167 | -- * task:kill() -- delete task and restart 168 | -- * task:statistics() -- return table with statistics 169 | local Task_methods = { 170 | start = function (self) 171 | -- if (get_fiber_id(self.worker_loop) ~= 0) then 172 | -- self.worker_loop:cancel() 173 | -- self.guardian_fiber = nil 174 | -- end 175 | self.guardian_fiber = fiber.create(guardian_loop, self) 176 | end, 177 | stop = function (self) 178 | if (get_fiber_id(self.guardian_fiber) ~= 0) then 179 | self.guardian_fiber:cancel() 180 | self.guardian_fiber = nil 181 | end 182 | if (get_fiber_id(self.worker_fiber) ~= 0) then 183 | self.worker_fiber:cancel() 184 | self.worker_fiber = nil 185 | end 186 | end, 187 | restart = function (self) 188 | self:stop() 189 | self:start() 190 | end, 191 | kill = function (self) 192 | self:stop() 193 | task_list[self.name] = nil 194 | end, 195 | statistics = function (self) 196 | return { 197 | checked_count = self.checked_tuples_count, 198 | expired_count = self.expired_tuples_count, 199 | restarts = self.restarts, 200 | working_time = math.floor(fiber.time() - self.start_time), 201 | } 202 | end, 203 | } 204 | 205 | -- create new expiration task 206 | local function create_task(name) 207 | local task = setmetatable({}, { __index = Task_methods }) 208 | task.name = name 209 | task.start_time = fiber.time() 210 | task.guardian_fiber = nil 211 | task.worker_fiber = nil 212 | task.space_id = nil 213 | task.expired_tuples_count = 0 214 | task.checked_tuples_count = 0 215 | task.restarts = 0 216 | task.is_tuple_expired = nil 217 | task.process_expired_tuple = nil 218 | task.args = nil 219 | task.tuples_per_iteration = constants.default_tuples_per_iteration 220 | task.full_scan_time = constants.default_full_scan_time 221 | return task 222 | end 223 | 224 | -- get task for table 225 | local function get_task(name) 226 | if name == nil then 227 | error("task name is nil") 228 | end 229 | 230 | -- check, does the task exist 231 | if task_list[name] == nil then 232 | error("task '" .. name .. "' doesn't exist") 233 | end 234 | 235 | return task_list[name] 236 | end 237 | 238 | -- default process_expired_tuple function 239 | local function default_tuple_drop(space_id, args, tuple) 240 | box.space[space_id]:delete(construct_key(space_id, tuple)) 241 | end 242 | 243 | 244 | -- ========================================================================= -- 245 | -- Expiration daemon management functions 246 | -- ========================================================================= -- 247 | 248 | -- Run a named task 249 | -- params: 250 | -- name -- task name 251 | -- space_id -- space to look in for expired tuples 252 | -- is_tuple_expired -- a function, must accept tuple and return 253 | -- true/false (is tuple expired or not), 254 | -- receives (args, tuple) as arguments 255 | -- options = { -- (table with named options) 256 | -- * process_expired_tuple -- applied to expired tuples, receives 257 | -- (space_id, args, tuple) as arguments 258 | -- * args -- passed to is_tuple_expired and 259 | -- process_expired_tuple() as additional context 260 | -- * tuples_per_iteration -- number of tuples will be checked by one iteration 261 | -- * full_scan_time -- time required for full index scan (in seconds) 262 | -- * force -- run task even on replica 263 | -- } 264 | local function expirationd_run_task(name, space_id, is_tuple_expired, options) 265 | if name == nil then 266 | error("task name is nil") 267 | end 268 | 269 | -- check, does the task exist 270 | local prev = task_list[name] 271 | if prev ~= nil then 272 | log.info("restart task %q", name) 273 | prev:kill(name) 274 | end 275 | local task = create_task(name) 276 | 277 | -- required params 278 | 279 | -- check expiration space number (required) 280 | if space_id == nil then 281 | error("space_id is nil") 282 | end 283 | task.space_id = space_id 284 | 285 | if is_tuple_expired == nil or type(is_tuple_expired) ~= "function" then 286 | error("is_tuple_expired is not a function, please provide a check function") 287 | end 288 | task.is_tuple_expired = is_tuple_expired 289 | 290 | -- optional params 291 | if options ~= nil and type(options) ~= 'table' then 292 | error("options must be table or not defined") 293 | end 294 | options = options or {} 295 | 296 | -- process expired tuple handler 297 | if options.process_expired_tuple and 298 | type(options.process_expired_tuple) ~= "function" then 299 | error("process_expired_tuple is not defined, please provide a purge function") 300 | end 301 | task.process_expired_tuple = options.process_expired_tuple or default_tuple_drop 302 | 303 | -- check expire and process after expiration handler's arguments 304 | task.args = options.args 305 | 306 | -- check tuples per iteration (not required) 307 | if options.tuples_per_iteration ~= nil then 308 | if options.tuples_per_iteration <= 0 then 309 | error("invalid tuples per iteration parameter") 310 | end 311 | task.tuples_per_iteration = options.tuples_per_iteration 312 | end 313 | 314 | -- check full scan time 315 | if options.full_scan_time ~= nil then 316 | if options.full_scan_time <= 0 then 317 | error("invalid full scan time") 318 | end 319 | task.full_scan_time = options.full_scan_time 320 | end 321 | 322 | if options.force ~= nil then 323 | if type(options.force) ~= 'boolean' then 324 | error("Invalid type of force value") 325 | end 326 | task.force = options.force 327 | end 328 | 329 | -- 330 | -- run task 331 | -- 332 | 333 | -- put the task to table 334 | task_list[name] = task 335 | -- run 336 | task:start() 337 | 338 | return task 339 | end 340 | 341 | local function expirationd_run_task_obsolete(name, 342 | space_id, 343 | is_tuple_expired, 344 | process_expired_tuple, 345 | args, 346 | tuples_per_iteration, 347 | full_scan_time) 348 | return expirationd_run_task( 349 | name, space_id, is_tuple_expired, { 350 | process_expired_tuple = process_expired_tuple, 351 | args = args, full_scan_time = full_scan_time, 352 | tuples_per_iteration = tuples_per_iteration, 353 | force = false, 354 | } 355 | ) 356 | end 357 | 358 | -- Kill named task 359 | -- params: 360 | -- name -- is task's name 361 | local function expirationd_kill_task(name) 362 | return get_task(name):kill() 363 | end 364 | 365 | -- Return copy of task list 366 | local function expirationd_show_task_list() 367 | return fun.map(function(x) return x end, fun.iter(task_list)):totable() 368 | end 369 | 370 | -- Return task statistics in table 371 | -- * checked_count - count of checked tuples (expired + skipped) 372 | -- * expired_count - count of expired tuples 373 | -- * restarts - count of task restarts 374 | -- * working_time - task operation time 375 | -- params: 376 | -- name -- task's name 377 | local function expirationd_task_stats(name) 378 | if name ~= nil then 379 | return get_task(name):statistics() 380 | end 381 | local retval = {} 382 | for name, task in pairs(task_list) do 383 | retval[name] = task:statistics() 384 | end 385 | return retval 386 | end 387 | 388 | -- kill task 389 | local function expirationd_kill_task(name) 390 | return get_task(name):kill() 391 | end 392 | 393 | -- get task by name 394 | local function expirationd_get_task(name) 395 | return get_task(name) 396 | end 397 | 398 | -- Update expirationd version in running tarantool 399 | -- * remove expirationd from package.loaded 400 | -- * require new expirationd 401 | -- * restart all tasks 402 | local function expirationd_update() 403 | local expd_prev = require('expirationd') 404 | package.loaded['expirationd'] = nil 405 | local expd_new = require('expirationd') 406 | for name, task in pairs(task_list) do 407 | task:kill() 408 | expd_new.start( 409 | task.name, task.space_id, 410 | task.is_tuple_expired, { 411 | process_expired_tuple = task.process_expired_tuple, 412 | args = task.args, tuples_per_iteration = task.tuples_per_iteration, 413 | full_scan_time = task.full_scan_time, force = task.force 414 | } 415 | ) 416 | end 417 | end 418 | 419 | return { 420 | start = expirationd_run_task, 421 | stats = expirationd_task_stats, 422 | update = expirationd_update, 423 | kill = expirationd_kill_task, 424 | task = expirationd_get_task, 425 | tasks = expirationd_show_task_list, 426 | -- Obsolete function names, use previous, instead 427 | task_stats = expirationd_task_stats, 428 | kill_task = expirationd_kill_task, 429 | get_task = expirationd_get_task, 430 | get_tasks = expirationd_show_task_list, 431 | run_task = expirationd_run_task_obsolete, 432 | show_task_list = expirationd_show_task_list, 433 | } 434 | -------------------------------------------------------------------------------- /lua/graphite.lua: -------------------------------------------------------------------------------- 1 | local fiber = require('fiber') 2 | local socket = require('socket') 3 | local log = require('log') 4 | 5 | local _M = { } 6 | local metrics = { } 7 | local initialized = false 8 | local common_stat_fiber = nil 9 | local stat_fiber = nil 10 | 11 | local sock = nil 12 | local host = '' 13 | local port = 0 14 | local prefix = '' 15 | 16 | local METRIC_SEC_TIMER = 0 17 | local METRIC_SUM_PER_MIN = 1 18 | local METRIC_SUM_PER_SEC = 2 19 | local METRIC_VALUE = 3 20 | local METRIC_AVG_PER_MIN = 4 21 | local METRIC_MIN_PER_MIN = 5 22 | local METRIC_MAX_PER_MIN = 6 23 | local METRIC_CALLBACK = 100 24 | 25 | local function send_graph(name, res, ts) 26 | if initialized == true then 27 | local graph = prefix .. name .. ' ' .. tostring(res) .. ' ' .. tostring(ts) .. '\n' 28 | sock:sendto(host, port, graph) 29 | end 30 | end 31 | 32 | local function send_metrics(ts, dt) 33 | for id, metric in pairs(metrics) do 34 | local mtype = metric[1] 35 | local name = metric[2] 36 | if mtype == METRIC_SEC_TIMER then 37 | local cnt = metric[3] 38 | local prev_cnt = metric[4] 39 | local values = metric[5] 40 | local aggr_fn = metric[7] 41 | 42 | if cnt > prev_cnt + 60 then 43 | prev_cnt = cnt - 60 44 | end 45 | 46 | if cnt ~= prev_cnt then 47 | local res = aggr_fn(prev_cnt, cnt - 1, values, dt) 48 | metric[4] = cnt 49 | send_graph(name, res, ts) 50 | end 51 | elseif mtype == METRIC_SUM_PER_MIN then 52 | local res = metric[3] 53 | send_graph(name, res, ts) 54 | metric[3] = 0 55 | elseif mtype == METRIC_SUM_PER_SEC then 56 | local res = metric[3] / dt 57 | send_graph(name, res, ts) 58 | metric[3] = 0 59 | elseif mtype == METRIC_VALUE then 60 | local res = metric[3] 61 | send_graph(name, res, ts) 62 | elseif mtype == METRIC_AVG_PER_MIN then 63 | local res = metric[3] 64 | if res ~= nil then 65 | metric[3] = nil 66 | if metric[5] > 1 then 67 | res = res + metric[4] / metric[5] 68 | end 69 | send_graph(name, res, ts) 70 | end 71 | elseif mtype == METRIC_MIN_PER_MIN then 72 | local res = metric[3] 73 | if res ~= nil then 74 | metric[3] = nil 75 | send_graph(name, res, ts) 76 | end 77 | elseif mtype == METRIC_MAX_PER_MIN then 78 | local res = metric[3] 79 | if res ~= nil then 80 | metric[3] = nil 81 | send_graph(name, res, ts) 82 | end 83 | elseif mtype == METRIC_CALLBACK then 84 | local res = metric[3]() 85 | if res ~= nil then 86 | if type(res) == 'table' then 87 | for _, m in ipairs(res) do 88 | send_graph(name .. '.' .. m[1], m[2], ts) 89 | end 90 | else 91 | send_graph(name, res, ts) 92 | end 93 | end 94 | end 95 | end 96 | end 97 | 98 | local function send_net_stats(ostats_net, stats_net, ts, dt) 99 | local res = 0 100 | 101 | res = (stats_net.SENT.total - ostats_net.SENT.total) / dt 102 | send_graph('net.sent_rps_avg', res, ts) 103 | send_graph('net.sent_total', stats_net.SENT.total, ts) 104 | 105 | if stats_net.EVENTS then 106 | res = (stats_net.EVENTS.total - ostats_net.EVENTS.total) / dt 107 | send_graph('net.events_rps_avg', res, ts) 108 | send_graph('net.events_total', stats_net.EVENTS.total, ts) 109 | end 110 | 111 | if stats_net.LOCKS then 112 | res = (stats_net.LOCKS.total - ostats_net.LOCKS.total) / dt 113 | send_graph('net.locks_rps_avg', res, ts) 114 | send_graph('net.locks_total', stats_net.LOCKS.total, ts) 115 | end 116 | 117 | res = (stats_net.RECEIVED.total - ostats_net.RECEIVED.total) / dt 118 | send_graph('net.received_rps_avg', res, ts) 119 | send_graph('net.received_total', stats_net.RECEIVED.total, ts) 120 | end 121 | 122 | local function send_mem_stats(stats_mem, ts, dt) 123 | -- https://www.tarantool.io/ru/doc/1.10/book/box/box_info/#box-info-memory 124 | -- Any stat written in bytes 125 | for k, v in pairs({ 126 | ['mem.cache_sz'] = stats_mem.cache, -- vinyl cache size 127 | ['mem.data_sz'] = stats_mem.data, -- arena 128 | ['mem.transactions_buf_sz'] = stats_mem.tx, -- vinyl transactions buffers size 129 | ['mem.lua_interp_sz'] = stats_mem.lua, -- lua interpreter mem (XXX: interpreter have a limit: 2GB) 130 | ['mem.net_io_buf_sz'] = stats_mem.net, -- net IO buffers size 131 | ['mem.index_sz'] = stats_mem.index, -- tarantool indexes size 132 | }) do 133 | send_graph(k, v, ts) 134 | end 135 | end 136 | 137 | local function send_box_stats(ostats_box, stats_box, ts, dt) 138 | local res = 0 139 | 140 | res = (stats_box.SELECT.total - ostats_box.SELECT.total) / dt 141 | send_graph('select_rps_avg', res, ts) 142 | 143 | res = (stats_box.REPLACE.total - ostats_box.REPLACE.total) / dt 144 | send_graph('replace_rps_avg', res, ts) 145 | 146 | res = (stats_box.UPDATE.total - ostats_box.UPDATE.total) / dt 147 | send_graph('update_rps_avg', res, ts) 148 | 149 | res = (stats_box.DELETE.total - ostats_box.DELETE.total) / dt 150 | send_graph('delete_rps_avg', res, ts) 151 | 152 | res = (stats_box.INSERT.total - ostats_box.INSERT.total) / dt 153 | send_graph('insert_rps_avg', res, ts) 154 | 155 | res = (stats_box.UPSERT.total - ostats_box.UPSERT.total) / dt 156 | send_graph('upsert_rps_avg', res, ts) 157 | 158 | res = (stats_box.CALL.total - ostats_box.CALL.total) / dt 159 | send_graph('call_rps_avg', res, ts) 160 | 161 | res = (stats_box.AUTH.total - ostats_box.AUTH.total) / dt 162 | send_graph('auth_rps_avg', res, ts) 163 | 164 | res = (stats_box.ERROR.total - ostats_box.ERROR.total) / dt 165 | send_graph('error_rps_avg', res, ts) 166 | end 167 | 168 | local function send_slab_stats(ts, dt) 169 | local slab_info = box.slab.info() 170 | for name, stat_ in pairs(slab_info) do 171 | local stat = string.gsub(stat_, '%%', '') 172 | send_graph(name, stat, ts) 173 | end 174 | 175 | if slab_info['quota_used'] and slab_info['quota_size'] then 176 | local quota_used = tonumber(slab_info['quota_used']) or 0 177 | local quota_size = tonumber(slab_info['quota_size']) or 0 178 | if quota_size > 0 then 179 | local quota_used_ratio = quota_used * 100 / quota_size 180 | send_graph("quota_used_ratio", quota_used_ratio, ts) 181 | end 182 | end 183 | 184 | for _, name in ipairs({ 185 | 'slab_alloc_arena', 186 | 'slab_alloc_factor', 187 | 'slab_alloc_minimal', 188 | 'slab_alloc_maximal', 189 | }) do 190 | if box.cfg[name] ~= nil then 191 | send_graph(name, box.cfg[name], ts) 192 | end 193 | end 194 | 195 | if _M.options.disable_slab_stats then 196 | return 197 | end 198 | 199 | local item_count = 0 200 | 201 | local slab_stats = box.slab.stats() 202 | for i, slab in pairs(slab_stats) do 203 | local item_size = slab['item_size'] 204 | local slab_prefix = 'slab_' .. tostring(item_size) .. '.' 205 | for name, stat in pairs(slab) do 206 | if name ~= 'item_size' then 207 | if name == 'item_count' then 208 | item_count = item_count + tonumber(stat) 209 | end 210 | send_graph(slab_prefix .. name, stat, ts) 211 | end 212 | end 213 | end 214 | 215 | send_graph('item_count', item_count, ts) 216 | end 217 | 218 | local function send_expirationd_stats(ts, dt) 219 | if not pcall(require, "expirationd") then 220 | return 221 | end 222 | 223 | local tasks = require("expirationd").stats() 224 | for task_name, task in pairs(tasks) do 225 | local task_prefix = 'expirationd.' .. task_name .. '.' 226 | for name, value in pairs(task) do 227 | if type(value) == "number" then 228 | local stat = string.gsub(name, "[.:]", "_") 229 | send_graph(task_prefix .. stat, value, ts) 230 | end 231 | end 232 | end 233 | end 234 | 235 | local function send_replication_stats(box_info, ts) 236 | local box_id = box_info.server.id or 0 237 | local box_lsn = box_info.server.lsn or 0 238 | local vclock = box_info['vclock'] 239 | 240 | local sum = 0 241 | for id, clock in ipairs(vclock) do 242 | send_graph('vclock.' .. tostring(id), clock, ts) 243 | sum = sum + clock 244 | end 245 | 246 | send_graph("id", box_id, ts) 247 | send_graph("lsn", box_lsn, ts) 248 | send_graph('vclock_sum', sum, ts) 249 | 250 | send_graph('replication_vclock_sum', sum - box_lsn, ts) 251 | if box_info.replication.status == "follow" then 252 | send_graph("replication_idle", box_info.replication.idle, ts) 253 | send_graph("replication_lag", box_info.replication.lag, ts) 254 | end 255 | end 256 | 257 | local function init_stats() 258 | _M.add_sec_metric('select_rps_max', function() return box.stat().SELECT.rps end, _M.max) 259 | _M.add_sec_metric('replace_rps_max', function() return box.stat().REPLACE.rps end, _M.max) 260 | _M.add_sec_metric('update_rps_max', function() return box.stat().UPDATE.rps end, _M.max) 261 | _M.add_sec_metric('insert_rps_max', function() return box.stat().INSERT.rps end, _M.max) 262 | _M.add_sec_metric('upsert_rps_max', function() return box.stat().UPSERT.rps end, _M.max) 263 | _M.add_sec_metric('call_rps_max', function() return box.stat().CALL.rps end, _M.max) 264 | _M.add_sec_metric('delete_rps_max', function() return box.stat().DELETE.rps end, _M.max) 265 | _M.add_sec_metric('auth_rps_max', function() return box.stat().AUTH.rps end, _M.max) 266 | _M.add_sec_metric('error_rps_max', function() return box.stat().ERROR.rps end, _M.max) 267 | end 268 | 269 | local function send_stats(ostats, stats, ts, dt) 270 | local res = 0 271 | 272 | ts = math.floor(ts) 273 | dt = math.floor(dt) 274 | 275 | if dt ~= 0 then 276 | local box_info = box.info 277 | 278 | -- send global stats 279 | send_graph("uptime", box_info.uptime or 0, ts) 280 | 281 | -- send net stats 282 | send_net_stats(ostats.net_stat, stats.net_stat, ts, dt) 283 | 284 | -- send box stats 285 | send_box_stats(ostats.base_stat, stats.base_stat, ts, dt) 286 | 287 | -- send slab stats 288 | send_slab_stats(ts, dt) 289 | 290 | -- send mem stats; available only on tarantools >= 1.7 291 | if stats.mem_stat ~= nil then 292 | send_mem_stats(stats.mem_stat, ts, dt) 293 | end 294 | 295 | -- send expirationd stats 296 | send_expirationd_stats(ts, dt) 297 | 298 | -- send replication stats 299 | send_replication_stats(box_info, ts) 300 | 301 | -- send custom metrics 302 | send_metrics(ts, dt) 303 | end 304 | end 305 | 306 | local function collect_stats() 307 | for id, metric in pairs(metrics) do 308 | local mtype = metric[1] 309 | if mtype == METRIC_SEC_TIMER then 310 | local cnt = metric[3] 311 | local values = metric[5] 312 | local metric_fn = metric[6] 313 | 314 | values[cnt % 60 + 1] = metric_fn() 315 | metric[3] = cnt + 1 316 | end 317 | end 318 | end 319 | 320 | _M.stop = function() 321 | for k, v in pairs(fiber.info()) do 322 | if string.find(v.name, 'graphite_common_stat') then 323 | log.info("killing fiber '%s' (%d)", v.name, v.fid) 324 | fiber.kill(v.fid) 325 | end 326 | end 327 | common_stat_fiber = nil 328 | 329 | for k, v in pairs(fiber.info()) do 330 | if string.find(v.name, 'graphite_stat') then 331 | log.info("killing fiber '%s' (%d)", v.name, v.fid) 332 | fiber.kill(v.fid) 333 | end 334 | end 335 | stat_fiber = nil 336 | 337 | if sock ~= nil then 338 | sock:close() 339 | sock = nil 340 | end 341 | 342 | metrics = {} 343 | initialized = false 344 | end 345 | 346 | _M.metrics = function() 347 | return metrics 348 | end 349 | 350 | local function accumulate_stat() 351 | return { 352 | base_stat = box.stat(), 353 | net_stat = box.stat.net(), 354 | mem_stat = box.info.memory and box.info.memory(), -- mem info available only on tarantools >= 1.7 355 | } 356 | end 357 | 358 | _M.init = function(prefix_, host_, port_, options_) 359 | prefix = prefix_ or 'localhost.tarantool.' 360 | host = host_ or 'nerv1.i' 361 | port = port_ or 2003 362 | 363 | _M.options = { 364 | disable_slab_stats = true, 365 | } 366 | for k, v in pairs(options_ or {}) do 367 | _M.options[k] = v 368 | end 369 | 370 | _M.stop() 371 | 372 | init_stats() 373 | initialized = true 374 | 375 | common_stat_fiber = fiber.create(function() 376 | fiber.name("graphite_common_stat") 377 | 378 | sock = socket('AF_INET', 'SOCK_DGRAM', 'udp') 379 | 380 | if sock ~= nil then 381 | local t = fiber.time() 382 | while true do 383 | local ostat = accumulate_stat() 384 | local nt = fiber.time() 385 | 386 | local st = 60 - (nt - t) 387 | fiber.sleep(st) 388 | 389 | local stat = accumulate_stat() 390 | 391 | t = fiber.time() 392 | send_stats(ostat, stat, t, t - nt) 393 | end 394 | end 395 | end) 396 | 397 | if common_stat_fiber ~= nil then 398 | stat_fiber = fiber.create(function() 399 | fiber.name("graphite_stat") 400 | 401 | while true do 402 | collect_stats() 403 | fiber.sleep(1) 404 | end 405 | end 406 | ) 407 | end 408 | 409 | log.info("Successfully initialized graphite module: %s:%s", host, port) 410 | end 411 | 412 | _M.sum = function(first, last, values, dt) 413 | local res = 0 414 | local i = first 415 | while i <= last do 416 | res = res + values[i % 60 + 1] 417 | i = i + 1 418 | end 419 | return res 420 | end 421 | 422 | _M.sum_per_sec = function(first, last, values, dt) 423 | local res = 0 424 | if dt ~= 0 then 425 | local i = first 426 | while i <= last do 427 | res = res + values[i % 60 + 1] 428 | i = i + 1 429 | end 430 | res = res / dt 431 | end 432 | return res 433 | end 434 | 435 | _M.max = function(first, last, values, dt) 436 | local res = nil 437 | local i = first 438 | while i <= last do 439 | local v = values[i % 60 + 1] 440 | if res == nil or v > res then 441 | res = v 442 | end 443 | i = i + 1 444 | end 445 | return res 446 | end 447 | 448 | _M.min = function(first, last, values, dt) 449 | local res = nil 450 | local i = first 451 | while i <= last do 452 | local v = values[i % 60 + 1] 453 | if res == nil or v < res then 454 | res = v 455 | end 456 | i = i + 1 457 | end 458 | return res 459 | end 460 | 461 | _M.last = function(first, last, values, dt) 462 | return values[last % 60 + 1] 463 | end 464 | 465 | _M.add_sec_metric = function(name, metric_fn, aggr_fn) 466 | local mtype = METRIC_SEC_TIMER 467 | local id = name .. '_' .. tostring(mtype) 468 | metrics[id] = { mtype, name, 0, 0, {}, metric_fn, aggr_fn } 469 | end 470 | 471 | _M.sum_per_min = function(name, value) 472 | local mtype = METRIC_SUM_PER_MIN 473 | local id = name .. '_' .. tostring(mtype) 474 | if metrics[id] == nil then 475 | metrics[id] = { mtype, name, value } 476 | else 477 | metrics[id][3] = metrics[id][3] + value 478 | end 479 | end 480 | 481 | _M.sum_per_sec = function(name, value) 482 | local mtype = METRIC_SUM_PER_SEC 483 | local id = name .. '_' .. tostring(mtype) 484 | if metrics[id] == nil then 485 | metrics[id] = { mtype, name, value } 486 | else 487 | metrics[id][3] = metrics[id][3] + value 488 | end 489 | end 490 | 491 | _M.add = function(name, value) 492 | local mtype = METRIC_VALUE 493 | local id = name .. '_' .. tostring(mtype) 494 | if metrics[id] == nil then 495 | metrics[id] = { mtype, name, value } 496 | else 497 | metrics[id][3] = metrics[id][3] + value 498 | end 499 | end 500 | 501 | _M.set = function(name, value) 502 | local mtype = METRIC_VALUE 503 | local id = name .. '_' .. tostring(mtype) 504 | metrics[id] = { mtype, name, value } 505 | end 506 | 507 | _M.avg_per_min = function(name, value) 508 | local mtype = METRIC_AVG_PER_MIN 509 | local id = name .. '_' .. tostring(mtype) 510 | if metrics[id] == nil or metrics[id][3] == nil then 511 | metrics[id] = { mtype, name, value, 0, 1 } 512 | else 513 | metrics[id][4] = metrics[id][4] + (value - metrics[id][3]) 514 | metrics[id][5] = metrics[id][5] + 1 515 | end 516 | end 517 | 518 | _M.min_per_min = function(name, value) 519 | local mtype = METRIC_MIN_PER_MIN 520 | local id = name .. '_' .. tostring(mtype) 521 | if metrics[id] == nil or metrics[id][3] == nil then 522 | metrics[id] = { mtype, name, value } 523 | else 524 | if value < metrics[id][3] then 525 | metrics[id][3] = value 526 | end 527 | end 528 | end 529 | 530 | _M.max_per_min = function(name, value) 531 | local mtype = METRIC_MAX_PER_MIN 532 | local id = name .. '_' .. tostring(mtype) 533 | if metrics[id] == nil or metrics[id][3] == nil then 534 | metrics[id] = { mtype, name, value } 535 | else 536 | if value > metrics[id][3] then 537 | metrics[id][3] = value 538 | end 539 | end 540 | end 541 | 542 | _M.send = function(name, res, ts) 543 | send_graph(name, res, math.floor(ts)) 544 | end 545 | 546 | _M.inc = function(name) 547 | _M.add(name, 1) 548 | end 549 | 550 | _M.callback = function(name, callback) 551 | if type(callback) ~= 'function' then 552 | log.error("callback is not a function") 553 | return 554 | end 555 | local mtype = METRIC_CALLBACK 556 | local id = name .. '_' .. tostring(mtype) 557 | metrics[id] = { mtype, name, callback } 558 | end 559 | 560 | _M.status = function() 561 | local status = {} 562 | 563 | status['initialized'] = initialized 564 | 565 | if initialized == true then 566 | status['fibers'] = {} 567 | 568 | if common_stat_fiber ~= nil then 569 | table.insert(status['fibers'], { 570 | name = common_stat_fiber:name(), 571 | status = common_stat_fiber:status() 572 | }) 573 | end 574 | 575 | if stat_fiber ~= nil then 576 | table.insert(status['fibers'], { 577 | name = stat_fiber:name(), 578 | status = stat_fiber:status() 579 | }) 580 | end 581 | 582 | status['host'] = host 583 | status['port'] = port 584 | status['prefix'] = prefix 585 | end 586 | 587 | return status 588 | end 589 | 590 | return _M 591 | -------------------------------------------------------------------------------- /lua/imagine.lua: -------------------------------------------------------------------------------- 1 | -- imagine helper functions for tarantool 1.6+ 2 | 3 | require('strict').on() 4 | 5 | local log = require('log') 6 | local pickle = require('pickle') 7 | local io = require('io') 8 | local fio = require('fio') 9 | local clock = require('clock') 10 | local digest = require('digest') 11 | local graphite = require('graphite') 12 | local ffi = require('ffi') 13 | local fiber = require('fiber') 14 | local yaml = require('yaml') 15 | 16 | ffi.cdef[[ 17 | int gethostname(char *name, size_t len); 18 | ]] 19 | 20 | -- module state ---------------------------------------------------------------- 21 | 22 | local config 23 | 24 | -- module state ^--------------------------------------------------------------- 25 | 26 | local function extend_deep(table_orig, table_by) 27 | for k, v in pairs(table_by) do 28 | if type(v) == 'table' then 29 | if table_orig[k] == nil then 30 | -- copy keys rather than refer the table 31 | table_orig[k] = extend_deep({}, v) 32 | elseif type(table_orig[k]) == 'table' then 33 | extend_deep(table_orig[k], v) 34 | else 35 | log.error("imagine: key types mismatch") 36 | end 37 | else 38 | table_orig[k] = v 39 | end 40 | end 41 | return table_orig 42 | end 43 | 44 | -- guess one level upper file from which this function was called 45 | local function guess_caller(skip_levels) 46 | local i = skip_levels or 2 47 | 48 | while true do 49 | local info = debug.getinfo(i, 'S') 50 | if not info then 51 | log.error("imagine: failed to determine module name") 52 | return 53 | end 54 | if not info.source:match('.*imagine%.lua$') then 55 | return info.source:match('^@.-([^/]+%.lua)$') 56 | end 57 | i = i + 1 58 | end 59 | end 60 | 61 | local function read_config(file) 62 | if not fio.stat(file) then 63 | log.info("imagine: configuration file not found, skipping (file:'%s')", file) 64 | return {} 65 | end 66 | 67 | local f, msg, errno = io.open(file, 'r') 68 | if not f then 69 | error(string.format("imagine: io.open failed (file:'%s', " .. 70 | "msg:'%s', errno:%d", file, msg, errno)) 71 | end 72 | local content = f:read('*all') 73 | f:close() 74 | if not content then 75 | error(string.format("imagine: file reading failed (file:'%s')", file)) 76 | end 77 | return yaml.decode(content) 78 | end 79 | 80 | local function print_config(config) 81 | local c = extend_deep({}, config) 82 | c.secure = c.secure and '' or nil 83 | log.info("imagine: using following configuration\n%s", yaml.encode(c)) 84 | end 85 | 86 | local function get_hostname() 87 | local buf = ffi.new('char[256]') 88 | ffi.C.gethostname(buf, 256) 89 | return ffi.string(buf) 90 | end 91 | 92 | local function normalize_metric_name(name) 93 | return name:lower():gsub('[^%w_]', '_') 94 | end 95 | 96 | --[[ 97 | -- packs sequential arguments to a table and 98 | -- sets number of elements (including nil holes) 99 | 100 | -- wrong (all data is lost after the nil hole): 101 | ret = {func(...)} 102 | return unpack(ret) 103 | 104 | -- correct: 105 | ret = pack(func(...)) 106 | return unpack(ret.r, 1, ret.n) 107 | 108 | ]]-- 109 | local function pack(...) 110 | return {r = {...}, n = select('#', ...)} 111 | end 112 | 113 | --[[ 114 | -- helps to create atomic function with one line 115 | -- it works like decorator 116 | -- very useful if you have multiple returns in your function 117 | 118 | -- usage example: 119 | your_function = imagine.atomic(yourfunction) 120 | 121 | ]]-- 122 | local function atomic_tail(status, ...) 123 | if not status then 124 | box.rollback() 125 | error((...), 2) 126 | end 127 | 128 | box.commit() 129 | return ... 130 | end 131 | 132 | local function atomic(func) 133 | return function(...) 134 | box.begin() 135 | return atomic_tail(pcall(func, ...)) 136 | end 137 | end 138 | 139 | --[[ 140 | -- init_storage works like decorator 141 | -- it helps to init storage for setuid function calls from client 142 | 143 | -- for example you have following init function which creates your spaces and indexes 144 | function init_modulename() 145 | box.schema.create_space('modulename', 146 | {if_not_exists = true}) 147 | box.space.modulename:create_index('pk', 148 | {type = 'hash', parts = {1, 'STR'}, if_not_exists = true}) 149 | end 150 | 151 | -- and you have theese two functions in your module as interface for client 152 | local function modulename.set(...) 153 | return box.space.modulename:replace(...) 154 | end 155 | 156 | local function modulename.get(...) 157 | return box.space.modulename:select(...) 158 | end 159 | 160 | -- you need to add following line to your code 161 | 162 | -- the parameters are: 163 | -- init_func: init function of your module which creates your spaces and indexes 164 | -- interface: interface functions of your module 165 | -- it will call box.schema.func.create for them 166 | imagine.init_storage(init_modulename, {'modulename.set', 'modulename.get'}) 167 | 168 | ]]-- 169 | local function init_storage(init_func, interface) 170 | local init_username = "imagine" 171 | box.schema.user.create(init_username, {if_not_exists = true}) 172 | box.schema.user.grant(init_username, 'execute,read,write', 'universe', nil, 173 | {if_not_exists = true}) 174 | box.session.su(init_username) 175 | 176 | init_func() 177 | 178 | for _, v in pairs(interface) do 179 | box.schema.func.create(v, {setuid = true, if_not_exists = true}) 180 | end 181 | 182 | box.session.su('admin') 183 | box.schema.user.revoke(init_username, 'execute,read,write', 'universe') 184 | end 185 | 186 | --[[ 187 | -- helps to create role with 'execute' access to interface functions 188 | -- use box.schema.user.grant('user_name', 'execute', 'role', 'role_name') to grant 189 | 190 | -- usage example: 191 | imagine.init_role('modulename_client', {'modulename.set', 'modulename.get'}) 192 | 193 | ]]-- 194 | local function init_role(role_name, interface) 195 | box.schema.role.create(role_name, {if_not_exists = true}) 196 | for _, v in pairs(interface) do 197 | box.schema.role.grant(role_name, 'execute', 'function', v, 198 | {if_not_exists = true}) 199 | end 200 | end 201 | 202 | local function stat_tail(name, ...) 203 | return ... 204 | end 205 | 206 | local function stat(name, func) 207 | local stat_name_func_call = 'imagine.func_' .. name .. '_call' 208 | return function(...) 209 | graphite.sum_per_min(stat_name_func_call, 1) 210 | return stat_tail(name, func(name, ...)) 211 | end 212 | end 213 | 214 | local function wrap_su_imagine(func) 215 | return function (...) 216 | local init_username, ret 217 | 218 | init_username = "imagine" 219 | box.schema.user.create(init_username, {if_not_exists = true}) 220 | box.schema.user.grant(init_username, 'execute,read,write', 'universe', nil, 221 | {if_not_exists = true}) 222 | box.session.su(init_username) 223 | 224 | ret = pack(func(...)) 225 | 226 | box.session.su('admin') 227 | box.schema.user.revoke(init_username, 'execute,read,write', 'universe') 228 | return unpack(ret.r, 1, ret.n) 229 | end 230 | end 231 | 232 | local function split(str, delim) 233 | local parts = {} 234 | 235 | for part in string.gmatch(str, '([^' .. delim .. ']+)') do 236 | parts[#parts + 1] = part 237 | end 238 | return parts 239 | end 240 | 241 | local function wrap_stat_init(name, options) 242 | local metrics, default_metrics, prefix, names, funcs, m, f, all_funcs 243 | 244 | default_metrics = 'avg,min,max,rpm' 245 | 246 | if type(name) ~= 'string' then 247 | log.error("imagine: 'name' is not a string") 248 | return 249 | end 250 | 251 | options = options or {} 252 | if type(options) ~= 'table' then 253 | log.error("imagine: 'options' is not a table") 254 | return 255 | end 256 | 257 | metrics = default_metrics 258 | if options.metrics then 259 | if type(options.metrics) ~= 'string' then 260 | log.error("imagine: 'options.metrics' is not a string") 261 | return 262 | end 263 | metrics = options.metrics 264 | end 265 | 266 | prefix = '' 267 | if options.prefix then 268 | if type(options.prefix) ~= 'string' then 269 | log.error("imagine: 'options.prefix' is not a string") 270 | return 271 | end 272 | prefix = options.prefix 273 | end 274 | 275 | names = { 276 | rpm = prefix .. name .. '_rpm', 277 | avg = prefix .. name .. '_avg', 278 | min = prefix .. name .. '_min', 279 | max = prefix .. name .. '_max', 280 | } 281 | all_funcs = { 282 | rpm = function (name) graphite.sum_per_min(name, 1) end, 283 | avg = graphite.avg_per_min, 284 | min = graphite.min_per_min, 285 | max = graphite.max_per_min, 286 | } 287 | 288 | funcs = {} 289 | for _, m in ipairs(split(metrics, ',')) do 290 | if all_funcs[m] then 291 | funcs[m] = all_funcs[m] 292 | else 293 | log.error("imagine: unsupported metric (name:'%s', metric:'%s')", 294 | name, m) 295 | end 296 | end 297 | 298 | return names, funcs 299 | end 300 | 301 | --[[ 302 | -- returns function wrapped with statistics 303 | -- 304 | -- parameters: 305 | -- name string identifier of counters 306 | -- func function to wrap 307 | -- options options 308 | -- 309 | -- options: 310 | -- prefix prefix for counters (default: '') 311 | -- metrics list of metrics (default: 'min,max,avg,rpm') 312 | -- available metrics: min, max, avg, rpm 313 | ]]-- 314 | local function wrap_stat(name, func, options) 315 | local names, funcs, stat_name 316 | 317 | stat_name = string.gsub(name, '[%.:]', '_') 318 | names, funcs = wrap_stat_init(stat_name, options) 319 | if not names then 320 | return func 321 | end 322 | return function (...) 323 | local start, delta, ret, m, f 324 | 325 | start = clock.time64() 326 | ret = pack(func(...)) 327 | delta = tonumber(clock.time64() - start) / 1000 328 | for m, f in pairs(funcs) do 329 | f(names[m], delta) 330 | end 331 | return unpack(ret.r, 1, ret.n) 332 | end 333 | end 334 | 335 | local wrap_stat_expirationd = (function () 336 | local wrapped_funcs = {} 337 | 338 | return function () 339 | local expirationd, task, v 340 | 341 | expirationd = require('expirationd') 342 | for _, v in pairs(expirationd.tasks()) do 343 | task = expirationd.task(v) 344 | if not wrapped_funcs[task.is_tuple_expired] then 345 | task.is_tuple_expired = wrap_stat(v .. '_exp', 346 | task.is_tuple_expired, {prefix = 'imagine.expd.'}) 347 | wrapped_funcs[task.is_tuple_expired] = true 348 | end 349 | if not wrapped_funcs[task.process_expired_tuple] then 350 | task.process_expired_tuple = wrap_stat(v .. '_proc', 351 | task.process_expired_tuple, {prefix = 'imagine.expd.'}) 352 | wrapped_funcs[task.process_expired_tuple] = true 353 | end 354 | end 355 | end 356 | end)() 357 | 358 | local function file_calc_hash(file) 359 | local f, err, errno, hash 360 | 361 | f, err, errno = io.open(file, 'rb') 362 | if not f then 363 | log.error("imagine: io.open failed (file:'%s', err:'%s', errno:%d)", 364 | file, err, errno) 365 | return 366 | end 367 | hash = digest.crc32(f:read('*all')) 368 | f:close() 369 | return hash 370 | end 371 | 372 | local file_add = (function() 373 | local hashes = {} 374 | 375 | return function(file) 376 | local key = file:match('([^/]+)%.lua$') or file:match('([^/]+)$') 377 | if not key then 378 | return 379 | end 380 | 381 | key = normalize_metric_name(key) 382 | local hash = file_calc_hash(file) 383 | hashes[key] = hash and math.fmod(hash, 1024) or -1024 384 | graphite.callback('imagine.files.' .. key, function () 385 | return hashes[key] 386 | end) 387 | end 388 | end)() 389 | 390 | local function init(options, do_re_require) 391 | 392 | if do_re_require ~= false then 393 | local function re_require(name) 394 | local module_prev = require(name) 395 | for k, _ in pairs(module_prev) do 396 | module_prev[k] = nil 397 | end 398 | package.loaded[name] = nil 399 | local module_new = require(name) 400 | for k, v in pairs(module_new) do 401 | module_prev[k] = v 402 | end 403 | return module_new 404 | end 405 | 406 | if package.loaded.expirationd ~= nil then 407 | local expirationd = require('expirationd') 408 | for _, name in pairs(expirationd.tasks()) do 409 | expirationd.task(name):kill() 410 | end 411 | 412 | re_require('expirationd') 413 | end 414 | 415 | graphite = re_require('graphite') 416 | 417 | return re_require('imagine').init(options, false) 418 | end 419 | 420 | local function init_role(name, options, all_options) 421 | local table, prefix, interface, func_name 422 | 423 | if type(name) ~= 'string' then 424 | log.error("imagine: 'options.roles' has non-string key") 425 | return 426 | end 427 | if type(options) ~= 'table' then 428 | log.error("imagine: 'options.roles.%s' is not a table", name) 429 | return 430 | end 431 | 432 | if type(options.funcs) ~= 'table' then 433 | log.error("imagine: 'options.roles.%s.funcs' is not specified " .. 434 | "or not a table", name) 435 | return 436 | end 437 | for k, v in pairs(options.funcs) do 438 | if type(k) ~= 'string' then 439 | log.error("imagine: 'options.roles.%s.funcs' has " .. 440 | "non-string key", name) 441 | return 442 | end 443 | if type(v) ~= 'function' then 444 | log.error("imagine: 'options.roles.%s.funcs.%s' is not a " .. 445 | "function", name, k) 446 | return 447 | end 448 | end 449 | 450 | if not options.table then 451 | table = _G 452 | prefix = '' 453 | elseif type(options.table) == 'string' then 454 | table = rawget(_G, options.table) 455 | if not table then 456 | table = {} 457 | rawset(_G, options.table, table) 458 | end 459 | prefix = options.table .. '.' 460 | else 461 | log.error("imagine: 'options.roles.%s.table' is not a string", 462 | name) 463 | return 464 | end 465 | 466 | interface = {} 467 | for k, func in pairs(options.funcs) do 468 | func_name = prefix .. k 469 | local func_table = table 470 | local func_name_obj = k:gsub(':', '.', 1) 471 | local func_name_split = split(func_name_obj, '.') 472 | if #func_name_split > 1 then 473 | k = func_name_split[#func_name_split] 474 | for i=1,#func_name_split - 1 do 475 | local new_table = rawget(func_table, func_name_split[i]) 476 | if not new_table then 477 | new_table = {} 478 | rawset(func_table, func_name_split[i], new_table) 479 | end 480 | func_table = new_table 481 | end 482 | end 483 | 484 | if not config.stat.no_auto_wrap_funcs then 485 | func = wrap_stat(func_name, func, {prefix = 'imagine.funcs.'}) 486 | end 487 | 488 | rawset(func_table, k, func) 489 | interface[#interface + 1] = func_name 490 | end 491 | 492 | if not box.cfg.read_only then 493 | box.schema.role.create(name, {if_not_exists = true}) 494 | for _, v in pairs(interface) do 495 | box.schema.func.create(v, {setuid = true, if_not_exists = true}) 496 | box.schema.role.grant(name, 'execute', 'function', v, 497 | {if_not_exists = true}) 498 | end 499 | end 500 | end 501 | 502 | if type(options) ~= 'table' then 503 | log.error("imagine: 'options' is not specified or not a table") 504 | return 505 | end 506 | if type(options.init_func) ~= 'function' then 507 | log.error("imagine: 'options.init_func' is not specified or " .. 508 | "not a function") 509 | return 510 | end 511 | if type(options.roles) ~= 'table' then 512 | log.error("imagine: 'options.roles' is not specified or not a table") 513 | return 514 | end 515 | 516 | options.name = options.name or guess_caller():match('(.+)%.lua$') 517 | 518 | do 519 | local config_graphite = config.graphite 520 | if options.graphite then 521 | if type(options.graphite) ~= 'table' then 522 | log.error("imagine: 'options.graphite' is not a table") 523 | return 524 | end 525 | config_graphite = extend_deep({}, config_graphite) 526 | config_graphite = extend_deep(config_graphite, options.graphite) 527 | end 528 | 529 | local prefix = string.format('%s.tnt.%s.%s.%s.', 530 | config_graphite.prefix, 531 | options.name, 532 | get_hostname():gsub('%..*', '.'), 533 | box.cfg.custom_proc_title) 534 | if config_graphite.raw_prefix then 535 | prefix = config_graphite.raw_prefix 536 | end 537 | graphite.init(prefix, config_graphite.ip, config_graphite.port) 538 | end 539 | 540 | -- XXX: options.init_func will be called only on master 541 | if not box.cfg.read_only then 542 | wrap_su_imagine(options.init_func)() 543 | end 544 | 545 | -- add size of spaces to statistics 546 | for _, v in box.space._space:pairs() do 547 | if v[1] >= 512 then 548 | local name = v[3] 549 | graphite.callback('imagine.space.' .. name, function () 550 | return box.space[name]:len() 551 | end) 552 | end 553 | end 554 | 555 | -- add fibers to statistics 556 | graphite.callback('imagine.fiber', function () 557 | local fibers = {} 558 | for _, v in pairs(fiber.info()) do 559 | fibers[#fibers + 1] = { normalize_metric_name(v.name), v.fid } 560 | end 561 | return fibers 562 | end) 563 | 564 | -- add files hash to statistics 565 | for fn in io.popen('ls -d -1 *.lua *.yaml 2>/dev/null'):lines() do 566 | file_add(fn) 567 | end 568 | 569 | -- create roles with interfaces 570 | if not box.cfg.read_only then 571 | for k, v in pairs(options.roles) do 572 | init_role(k, v, options) 573 | end 574 | end 575 | 576 | if package.loaded.expirationd ~= nil then 577 | wrap_stat_expirationd() 578 | end 579 | end 580 | 581 | return (function () 582 | -- put all module initializations here, 583 | -- instead of spreading them over the file 584 | config = {} 585 | config.stat = {} 586 | config.graphite = { 587 | prefix = '__PROJECT__', 588 | ip = '127.0.0.1', 589 | port = 2003, 590 | } 591 | extend_deep(config, rawget(_G, 'imagine_default_config') or {}) 592 | extend_deep(config, read_config('imagine.conf.yaml')) 593 | print_config(config) 594 | 595 | return { 596 | init = init, 597 | init_storage = init_storage, 598 | init_role = init_role, 599 | get_hostname = get_hostname, 600 | 601 | config = config, 602 | 603 | atomic = atomic, 604 | } 605 | end)() 606 | --------------------------------------------------------------------------------