From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id 4629F2EC72 for ; Wed, 15 May 2019 15:36:51 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id AfZeiTnUO_22 for ; Wed, 15 May 2019 15:36:51 -0400 (EDT) Received: from smtpng1.m.smailru.net (smtpng1.m.smailru.net [94.100.181.251]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id C5C6E2EC70 for ; Wed, 15 May 2019 15:36:50 -0400 (EDT) From: Vladislav Shpilevoy Subject: [tarantool-patches] [PATCH 04/10] swim: introduce Lua interface Date: Wed, 15 May 2019 22:36:41 +0300 Message-Id: <521afc07e66b44f44cd8956d7599942a57f0864d.1557948687.git.v.shpilevoy@tarantool.org> In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-Help: List-Unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-Subscribe: List-Owner: List-post: List-Archive: To: tarantool-patches@freelists.org Cc: kostja@tarantool.org SWIM as a library can be useful not only for server internals, but for users as well. This commit exposes Lua bindings to SWIM C API. Here only basic bindings are introduced to create, delete, quit, check a SWIM instance. With sanity tests. Part of #3234 --- extra/exports | 25 +++ src/CMakeLists.txt | 1 + src/lua/init.c | 2 + src/lua/swim.lua | 408 ++++++++++++++++++++++++++++++++++++++++ test/swim/box.lua | 24 +++ test/swim/suite.ini | 5 + test/swim/swim.result | 201 ++++++++++++++++++++ test/swim/swim.test.lua | 68 +++++++ 8 files changed, 734 insertions(+) create mode 100644 src/lua/swim.lua create mode 100644 test/swim/box.lua create mode 100644 test/swim/suite.ini create mode 100644 test/swim/swim.result create mode 100644 test/swim/swim.test.lua diff --git a/extra/exports b/extra/exports index 5375a01e4..b0e20e11f 100644 --- a/extra/exports +++ b/extra/exports @@ -87,6 +87,31 @@ tnt_HMAC_CTX_free lua_static_aligned_alloc +swim_new +swim_is_configured +swim_cfg +swim_set_payload +swim_delete +swim_add_member +swim_remove_member +swim_probe_member +swim_broadcast +swim_size +swim_quit +swim_self +swim_member_by_uuid +swim_member_status +swim_iterator_open +swim_iterator_next +swim_iterator_close +swim_member_uri +swim_member_uuid +swim_member_incarnation +swim_member_payload +swim_member_ref +swim_member_unref +swim_member_is_dropped + # Module API _say diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 492b8712e..c2e0240ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -48,6 +48,7 @@ lua_source(lua_sources lua/table.lua) lua_source(lua_sources ../third_party/luafun/fun.lua) lua_source(lua_sources lua/httpc.lua) lua_source(lua_sources lua/iconv.lua) +lua_source(lua_sources lua/swim.lua) # LuaJIT jit.* library lua_source(lua_sources "${CMAKE_BINARY_DIR}/third_party/luajit/src/jit/bc.lua") lua_source(lua_sources "${CMAKE_BINARY_DIR}/third_party/luajit/src/jit/bcsave.lua") diff --git a/src/lua/init.c b/src/lua/init.c index 303369841..5ddc5a4d8 100644 --- a/src/lua/init.c +++ b/src/lua/init.c @@ -115,6 +115,7 @@ extern char strict_lua[], table_lua[], trigger_lua[], string_lua[], + swim_lua[], p_lua[], /* LuaJIT 2.1 profiler */ zone_lua[] /* LuaJIT 2.1 profiler */; @@ -149,6 +150,7 @@ static const char *lua_modules[] = { "pwd", pwd_lua, "http.client", httpc_lua, "iconv", iconv_lua, + "swim", swim_lua, /* jit.* library */ "jit.vmdef", vmdef_lua, "jit.bc", bc_lua, diff --git a/src/lua/swim.lua b/src/lua/swim.lua new file mode 100644 index 000000000..bca98627c --- /dev/null +++ b/src/lua/swim.lua @@ -0,0 +1,408 @@ +local ffi = require('ffi') +local uuid = require('uuid') + +ffi.cdef[[ + struct swim; + struct tt_uuid; + struct swim_iterator; + struct swim_member; + + enum swim_gc_mode { + SWIM_GC_DEFAULT = -1, + SWIM_GC_OFF = 0, + SWIM_GC_ON = 1, + }; + + enum swim_member_status { + MEMBER_ALIVE = 0, + MEMBER_SUSPECTED, + MEMBER_DEAD, + MEMBER_LEFT, + }; + + struct swim * + swim_new(void); + + bool + swim_is_configured(const struct swim *swim); + + int + swim_cfg(struct swim *swim, const char *uri, double heartbeat_rate, + double ack_timeout, enum swim_gc_mode gc_mode, + const struct tt_uuid *uuid); + + int + swim_set_payload(struct swim *swim, const char *payload, int payload_size); + + void + swim_delete(struct swim *swim); + + int + swim_add_member(struct swim *swim, const char *uri, + const struct tt_uuid *uuid); + + int + swim_remove_member(struct swim *swim, const struct tt_uuid *uuid); + + int + swim_probe_member(struct swim *swim, const char *uri); + + int + swim_broadcast(struct swim *swim, int port); + + int + swim_size(const struct swim *swim); + + void + swim_quit(struct swim *swim); + + struct swim_member * + swim_self(struct swim *swim); + + struct swim_member * + swim_member_by_uuid(struct swim *swim, const struct tt_uuid *uuid); + + enum swim_member_status + swim_member_status(const struct swim_member *member); + + struct swim_iterator * + swim_iterator_open(struct swim *swim); + + struct swim_member * + swim_iterator_next(struct swim_iterator *iterator); + + void + swim_iterator_close(struct swim_iterator *iterator); + + const char * + swim_member_uri(const struct swim_member *member); + + const struct tt_uuid * + swim_member_uuid(const struct swim_member *member); + + uint64_t + swim_member_incarnation(const struct swim_member *member); + + const char * + swim_member_payload(const struct swim_member *member, int *size); + + void + swim_member_ref(struct swim_member *member); + + void + swim_member_unref(struct swim_member *member); + + bool + swim_member_is_dropped(const struct swim_member *member); +]] + +-- Shortcut to avoid unnecessary lookups in 'ffi' table. +local capi = ffi.C + +local swim_t = ffi.typeof('struct swim *') + +-- +-- Check if @a value is something that can be passed as a +-- URI parameter. Note, it does not validate URI, because it is +-- done in C module. Here only dynamic typing errors are checked. +-- Throws on invalid type. +-- +-- @param value Value to check and probably convert. +-- @param func_name Caller function name to include into an error +-- message. +-- @return String that can be passed as a URI parameter. +-- +local function swim_check_uri(value, func_name) + if value == nil then + return nil + end + if type(value) == 'string' then + return value + end + if type(value) == 'number' then + return tostring(value) + end + return error(func_name..': expected string URI or port number') +end + +-- +-- Check if @a value is a number, that can be passed as a timeout. +-- Throws on invalid type. +-- +-- @param value Value to check. +-- @param func_name Caller function name to include into an error +-- message. +-- @param param_name Timeout parameter name to include into an +-- error message. Examples: 'heartbeat_rate', +-- 'ack_timeout'. +-- @return Timeout value. Can be negative - SWIM treats negative +-- value as an instruction to keep an old timeout. +-- +local function swim_check_timeout(value, func_name, param_name) + if value == nil then + return -1 + end + if type(value) ~= 'number' then + return error(func_name..': expected number '..param_name) + end + return value +end + +-- +-- Check if @a value is a valid garbage collection mode. +-- Throws on invalid type and unknown mode. +-- +-- @param value Value to check and convert. +-- @param func_name Caller function name to include into an error +-- message. +-- @return GC mode cdata enum value. +-- +local function swim_check_gc_mode(value, func_name) + if value == nil then + return capi.SWIM_GC_DEFAULT + end + if value == 'on' then + return capi.SWIM_GC_ON + elseif value == 'off' then + return capi.SWIM_GC_OFF + else + return error(func_name..': unknown gc_mode') + end +end + +-- +-- Check if @a value is a valid UUID. Throws on invalid type and +-- UUID. +-- +-- @param value Value to check. +-- @param func_name Caller function name to include into an error +-- message. +-- @return Struct UUID cdata. +-- +local function swim_check_uuid(value, func_name) + if value == nil then + return nil + end + if type(value) ~= 'string' then + return error(func_name..': expected string UUID') + end + value = uuid.fromstr(value) + if not value then + return error(func_name..': invalid UUID') + end + return value +end + +-- +-- Check if @a s is a SWIM instance. It should be a table with +-- cdata struct swim in 'ptr' attribute. Throws on invalid type. +-- +-- @param value Value to check. +-- @param func_name Caller function name to include into an error +-- message. +-- @return Pointer to struct swim. +-- +local function swim_check_instance(s, func_name) + if type(s) == 'table' then + local ptr = s.ptr + if ffi.istype(swim_t, ptr) then + return ptr + end + end + return error(func_name..': first argument is not a SWIM instance') +end + +-- +-- When a SWIM instance is deleted or has quited, it can't be used +-- anymore. This function replaces all methods of a deleted +-- instance to throw an error on a usage attempt. +-- +local function swim_error_deleted() + return error('the swim instance is deleted') +end +-- This is done without 'if' in the original methods, but rather +-- via metatable replacement after deletion has happened. +local swim_mt_deleted = { + __index = swim_error_deleted +} + +-- +-- Delete a SWIM instance immediately, do not notify cluster +-- members about that. +-- +local function swim_delete(s) + local ptr = swim_check_instance(s, 'swim:delete') + capi.swim_delete(ffi.gc(ptr, nil)) + s.ptr = nil + setmetatable(s, swim_mt_deleted) +end + +-- +-- Quit from a cluster gracefully, notify other members. The SWIM +-- instance is considered deleted immediately after this function +-- returned, and can't be used anymore. +-- +local function swim_quit(s) + local ptr = swim_check_instance(s, 'swim:quit') + capi.swim_quit(ffi.gc(ptr, nil)) + s.ptr = nil + setmetatable(s, swim_mt_deleted) +end + +-- +-- Size of the local member table. +-- +local function swim_size(s) + return capi.swim_size(swim_check_instance(s, 'swim:size')) +end + +-- +-- Check if a SWIM instance is configured already. Not configured +-- instance does not provide any methods. +-- +local function swim_is_configured(s) + return capi.swim_is_configured(swim_check_instance(s, 'swim:is_configured')) +end + +-- +-- Configuration options are printed when a SWIM instance is +-- serialized, for example, in a console. +-- +local function swim_serialize(s) + return s.cfg.index +end + +-- +-- Normal metatable of a configured SWIM instance. +-- +local swim_mt = { + __index = { + delete = swim_delete, + quit = swim_quit, + size = swim_size, + is_configured = swim_is_configured, + }, + __serialize = swim_serialize +} + +local swim_cfg_options = { + uri = true, heartbeat_rate = true, ack_timeout = true, + gc_mode = true, uuid = true +} + +-- +-- SWIM 'cfg' attribute is not a trivial table nor function. It is +-- a callable table. It allows to strongly restrict use cases of +-- swim.cfg down to 2 applications: +-- +-- - Cfg-table. 'swim.cfg' is a read-only table of configured +-- parameters. A user can't write 'swim.cfg. = value' - an +-- error is thrown. But it can be indexed like +-- 'opt = swim.cfg.'. Configuration is cached in Lua, so +-- the latter example won't even call SWIM C API functions. +-- +-- - Cfg-function. 'swim:cfg()' reconfigures a SWIM +-- instance and returns normal values: nil+error or true. The +-- new configuration is cached for further indexing. +-- +-- All the other combinations are banned. The function below +-- implements configuration call. +-- +local function swim_cfg_call(c, s, cfg) + local func_name = 'swim:cfg' + local ptr = swim_check_instance(s, func_name) + if type(cfg) ~= 'table' then + return error(func_name..': expected table configuration') + end + for k in pairs(cfg) do + if not swim_cfg_options[k] then + return error(func_name..': unknown option '..k) + end + end + local uri = swim_check_uri(cfg.uri, func_name) + local heartbeat_rate = swim_check_timeout(cfg.heartbeat_rate, + func_name, 'heartbeat_rate') + local ack_timeout = swim_check_timeout(cfg.ack_timeout, func_name, + 'ack_timeout'); + local gc_mode = swim_check_gc_mode(cfg.gc_mode, func_name) + local uuid = swim_check_uuid(cfg.uuid, func_name) + if capi.swim_cfg(ptr, uri, heartbeat_rate, ack_timeout, + gc_mode, uuid) ~= 0 then + return nil, box.error.last() + end + local index = c.index + for k, v in pairs(cfg) do + index[k] = v + end + return true +end + +local swim_cfg_mt = { + __call = swim_cfg_call, + __index = function(c, k) + return c.index[k] + end, + __serialize = function(c) + return c.index + end, + __newindex = function() + return error('please, use swim:cfg{key = value} instead of '.. + 'swim.cfg.key = value') + end +} + +-- +-- First 'swim:cfg()' call is different from others. On success it +-- replaces swim metatable with a full one, where all the variety +-- of methods is available. +-- +local function swim_cfg_first_call(c, s, cfg) + local ok, err = swim_cfg_call(c, s, cfg) + if not ok then + return ok, err + end + -- Update 'cfg' metatable as well to never call this + -- function again and use ordinary swim_cfg_call() directly. + setmetatable(c, swim_cfg_mt) + setmetatable(s, swim_mt) + return ok +end + +local swim_not_configured_mt = { + __index = { + delete = swim_delete, + is_configured = swim_is_configured, + }, + __serialize = swim_serialize +} + +local swim_cfg_not_configured_mt = table.deepcopy(swim_cfg_mt) +swim_cfg_not_configured_mt.__call = swim_cfg_first_call + +-- +-- Create a new SWIM instance, and configure if @a cfg is +-- provided. +-- +local function swim_new(cfg) + local ptr = capi.swim_new() + if ptr == nil then + return nil, box.error.last() + end + ffi.gc(ptr, capi.swim_delete) + local s = setmetatable({ + ptr = ptr, + cfg = setmetatable({index = {}}, swim_cfg_not_configured_mt) + }, swim_not_configured_mt) + if cfg then + local ok, err = s:cfg(cfg) + if not ok then + s:delete() + return ok, err + end + end + return s +end + +return { + new = swim_new, +} diff --git a/test/swim/box.lua b/test/swim/box.lua new file mode 100644 index 000000000..b6a39575e --- /dev/null +++ b/test/swim/box.lua @@ -0,0 +1,24 @@ +#!/usr/bin/env tarantool + +swim = require('swim') +fiber = require('fiber') +listen_uri = tostring(os.getenv("LISTEN")) +listen_port = require('uri').parse(listen_uri).service + +box.cfg{} + +function uuid(i) + local min_valid_prefix = '00000000-0000-1000-8000-' + if i < 10 then + return min_valid_prefix..'00000000000'..tostring(i) + end + assert(i < 100) + return min_valid_prefix..'0000000000'..tostring(i) +end + +function uri(port) + port = port or 0 + return '127.0.0.1:'..tostring(port) +end + +require('console').listen(os.getenv('ADMIN')) diff --git a/test/swim/suite.ini b/test/swim/suite.ini new file mode 100644 index 000000000..13189c1cf --- /dev/null +++ b/test/swim/suite.ini @@ -0,0 +1,5 @@ +[default] +core = tarantool +description = SWIM tests +script = box.lua +is_parallel = True diff --git a/test/swim/swim.result b/test/swim/swim.result new file mode 100644 index 000000000..2e5025da6 --- /dev/null +++ b/test/swim/swim.result @@ -0,0 +1,201 @@ +test_run = require('test_run').new() +--- +... +test_run:cmd("push filter '\\.lua.*:[0-9]+: ' to '.lua:: '") +--- +- true +... +-- +-- gh-3234: SWIM gossip protocol. +-- +-- Invalid cfg parameters. +swim.new(1) +--- +- error: 'builtin/swim.lua:: swim:cfg: expected table configuration' +... +swim.new({uri = true}) +--- +- error: 'builtin/swim.lua:: swim:cfg: expected string URI or port number' +... +swim.new({heartbeat_rate = 'rate'}) +--- +- error: 'builtin/swim.lua:: swim:cfg: expected number heartbeat_rate' +... +swim.new({ack_timeout = 'timeout'}) +--- +- error: 'builtin/swim.lua:: swim:cfg: expected number ack_timeout' +... +swim.new({gc_mode = 'not a mode'}) +--- +- error: 'builtin/swim.lua:: swim:cfg: unknown gc_mode' +... +swim.new({gc_mode = 0}) +--- +- error: 'builtin/swim.lua:: swim:cfg: unknown gc_mode' +... +swim.new({uuid = 123}) +--- +- error: 'builtin/swim.lua:: swim:cfg: expected string UUID' +... +swim.new({uuid = '1234'}) +--- +- error: 'builtin/swim.lua:: swim:cfg: invalid UUID' +... +-- Valid parameters, but invalid configuration. +swim.new({}) +--- +- null +- 'swim.cfg: UUID and URI are mandatory in a first config' +... +swim.new({uuid = uuid(1)}) +--- +- null +- 'swim.cfg: UUID and URI are mandatory in a first config' +... +swim.new({uri = uri()}) +--- +- null +- 'swim.cfg: UUID and URI are mandatory in a first config' +... +-- Check manual deletion. +s = swim.new({uuid = uuid(1), uri = uri()}) +--- +... +s:delete() +--- +... +s:cfg({}) +--- +- error: 'builtin/swim.lua:: the swim instance is deleted' +... +s = nil +--- +... +_ = collectgarbage('collect') +--- +... +s = swim.new({uuid = uuid(1), uri = uri()}) +--- +... +s:quit() +--- +... +s:is_configured() +--- +- error: '[string "return s:is_configured() "]:1: the swim instance is deleted' +... +s = nil +--- +... +_ = collectgarbage('collect') +--- +... +s = swim.new({uuid = uuid(1), uri = uri()}) +--- +... +s:is_configured() +--- +- true +... +s:size() +--- +- 1 +... +s.cfg +--- +- uuid: 00000000-0000-1000-8000-000000000001 + uri: 127.0.0.1:0 +... +s.cfg.gc_mode = 'off' +--- +- error: '[string "s.cfg.gc_mode = ''off'' "]:1: please, use swim:cfg{key = value} + instead of swim.cfg.key = value' +... +s:cfg{gc_mode = 'off'} +--- +- true +... +s.cfg +--- +- gc_mode: off + uuid: 00000000-0000-1000-8000-000000000001 + uri: 127.0.0.1:0 +... +s.cfg.gc_mode +--- +- off +... +s.cfg.uuid +--- +- 00000000-0000-1000-8000-000000000001 +... +s.cfg() +--- +- error: 'builtin/swim.lua:: swim:cfg: first argument is not a SWIM instance' +... +s:cfg({wrong_opt = 100}) +--- +- error: 'swim:cfg: unknown option wrong_opt' +... +s:delete() +--- +... +-- Reconfigure. +s = swim.new() +--- +... +s:is_configured() +--- +- false +... +-- Check that not configured instance does not provide most of +-- methods. +s.quit == nil +--- +- true +... +s:cfg({uuid = uuid(1), uri = uri()}) +--- +- true +... +s.quit ~= nil +--- +- true +... +s:is_configured() +--- +- true +... +s:size() +--- +- 1 +... +s = nil +--- +... +_ = collectgarbage('collect') +--- +... +-- Invalid usage. +s = swim.new() +--- +... +s.delete() +--- +- error: 'builtin/swim.lua:: swim:delete: first argument is not a SWIM instance' +... +s.is_configured() +--- +- error: 'builtin/swim.lua:: swim:is_configured: first argument is not a SWIM instance' +... +s.cfg() +--- +- error: 'builtin/swim.lua:: swim:cfg: first argument is not a SWIM instance' +... +s:delete() +--- +... +test_run:cmd("clear filter") +--- +- true +... diff --git a/test/swim/swim.test.lua b/test/swim/swim.test.lua new file mode 100644 index 000000000..719783133 --- /dev/null +++ b/test/swim/swim.test.lua @@ -0,0 +1,68 @@ +test_run = require('test_run').new() +test_run:cmd("push filter '\\.lua.*:[0-9]+: ' to '.lua:: '") +-- +-- gh-3234: SWIM gossip protocol. +-- + +-- Invalid cfg parameters. +swim.new(1) +swim.new({uri = true}) +swim.new({heartbeat_rate = 'rate'}) +swim.new({ack_timeout = 'timeout'}) +swim.new({gc_mode = 'not a mode'}) +swim.new({gc_mode = 0}) +swim.new({uuid = 123}) +swim.new({uuid = '1234'}) + +-- Valid parameters, but invalid configuration. +swim.new({}) +swim.new({uuid = uuid(1)}) +swim.new({uri = uri()}) + +-- Check manual deletion. +s = swim.new({uuid = uuid(1), uri = uri()}) +s:delete() +s:cfg({}) +s = nil +_ = collectgarbage('collect') + +s = swim.new({uuid = uuid(1), uri = uri()}) +s:quit() +s:is_configured() +s = nil +_ = collectgarbage('collect') + +s = swim.new({uuid = uuid(1), uri = uri()}) +s:is_configured() +s:size() +s.cfg +s.cfg.gc_mode = 'off' +s:cfg{gc_mode = 'off'} +s.cfg +s.cfg.gc_mode +s.cfg.uuid +s.cfg() +s:cfg({wrong_opt = 100}) +s:delete() + +-- Reconfigure. +s = swim.new() +s:is_configured() +-- Check that not configured instance does not provide most of +-- methods. +s.quit == nil +s:cfg({uuid = uuid(1), uri = uri()}) +s.quit ~= nil +s:is_configured() +s:size() +s = nil +_ = collectgarbage('collect') + +-- Invalid usage. +s = swim.new() +s.delete() +s.is_configured() +s.cfg() +s:delete() + +test_run:cmd("clear filter") -- 2.20.1 (Apple Git-117)