diff --git a/.github/workflows/reusable_testing.yml b/.github/workflows/reusable_testing.yml index 55d63be..309bb64 100644 --- a/.github/workflows/reusable_testing.yml +++ b/.github/workflows/reusable_testing.yml @@ -31,13 +31,6 @@ jobs: # dependencies when migrating to other OS version. run: sudo dpkg -i tarantool_*.deb tarantool-common_*.deb tarantool-dev_*.deb - # TODO: Remove this when https://github.com/tarantool/memcached/issues/96 - # is resolved. - - name: Get the tarantool version - run: | - TNT_VERSION=$(tarantool --version | grep -e '^Tarantool') - echo "TNT_VERSION=$TNT_VERSION" >> $GITHUB_ENV - - name: Setup python3 for tests uses: actions/setup-python@v2 with: @@ -51,7 +44,3 @@ jobs: - run: cmake . && make - run: make test-memcached - # TODO: Remove this when https://github.com/tarantool/memcached/issues/96 - # is resolved. - if: ${{ startsWith(env.TNT_VERSION, 'Tarantool 1.10') || - startsWith(env.TNT_VERSION, 'Tarantool 2.8') }} diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 4c2f5ef..fa7ca73 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -11,7 +11,7 @@ jobs: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository strategy: matrix: - tarantool-version: ['1.10', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '2.8'] + tarantool-version: ['1.10', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '2.8', '2.x-latest'] fail-fast: false runs-on: [ubuntu-latest] steps: @@ -23,11 +23,18 @@ jobs: # for running tests. submodules: recursive - - name: Setup Tarantool + - name: Setup Tarantool (version is not equal to latest 2.x) + if: matrix.tarantool-version != '2.x-latest' uses: tarantool/setup-tarantool@v1 with: tarantool-version: ${{ matrix.tarantool-version }} + - name: Setup Tarantool 2.x (latest) + if: matrix.tarantool-version == '2.x-latest' + run: | + curl -L https://tarantool.io/pre-release/2/installer.sh | sudo bash + sudo apt install -y tarantool tarantool-dev + - name: Install build requirements run: sudo apt-get -y install libsasl2-dev libevent-dev diff --git a/.luacheckrc b/.luacheckrc index ee60822..5f93d81 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,6 +1,7 @@ std = "luajit" globals = { "box", + "tonumber64", } ignore = { -- Accessing an undefined field of a global variable . diff --git a/README.md b/README.md index ab30a07..6c089ac 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ END ## API * `local memcached = require('memcached')` - acquire a library handle +* `local slab = memcached.slab.info()` - show information about slab arena and quota. * `local instance = memcached.create(, , )` - create a new instance, register it and run - `name` - a string with instance name - `uri` - a string with uri to bind to, for example: `0.0.0.0:11211` (only TCP is supported now) diff --git a/memcached/CMakeLists.txt b/memcached/CMakeLists.txt index a928868..f03b588 100644 --- a/memcached/CMakeLists.txt +++ b/memcached/CMakeLists.txt @@ -29,6 +29,7 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated") add_library(internalso SHARED + "internal/alloc.c" "internal/constants.c" "internal/utils.c" "internal/proto_bin.c" diff --git a/memcached/init.lua b/memcached/init.lua index 2b991c0..2b8626b 100644 --- a/memcached/init.lua +++ b/memcached/init.lua @@ -54,6 +54,12 @@ struct memcached_stat { uint64_t auth_errors; }; +struct slab_arena_info { + size_t quota_size; + size_t quota_used; + double quota_used_ratio; +}; + void memcached_set_opt (struct memcached_service *srv, int opt, ...); @@ -82,6 +88,13 @@ memcached_get_stat (struct memcached_service *); struct memcached_service * memcached_create(const char *, uint32_t); +void memcached_slab_arena_create(); + +void memcached_slab_cache_create(); + +struct slab_arena_info * +memcached_slab_arena_info(); + int memcached_start (struct memcached_service *); void memcached_stop (struct memcached_service *); void memcached_free (struct memcached_service *); @@ -233,6 +246,12 @@ local RUNNING = 'r' local STOPPED = 's' local ERRORED = 'e' +local slab_arena_info_table = { + 'quota_size', + 'quota_used', + 'quota_used_ratio', +} + local stat_table = { 'total_items', 'curr_items', 'curr_conns', 'total_conns', @@ -332,7 +351,7 @@ local memcached_methods = { end } -local function memcached_init(name, uri, opts) +local function memcached_create_instance(name, uri, opts) opts = opts or {} if memcached_services[name] ~= nil then if not opts.if_not_exists then @@ -386,8 +405,29 @@ local function memcached_get(name) return memcached_services[name] end +local function memcached_slab_info() + local slab_arena_info = memcached_internal.memcached_slab_arena_info() + local slab_info = {} + for _, v in pairs(slab_arena_info_table) do + slab_info[v] = tonumber64(slab_arena_info[0][v]) + end + slab_info['quota_used_ratio'] = string.format('%0.2f%%', slab_info['quota_used_ratio']) + + return slab_info +end + +local function memcached_init() + memcached_internal.memcached_slab_arena_create() + memcached_internal.memcached_slab_cache_create() +end + +memcached_init() + return { - create = memcached_init, + create = memcached_create_instance, + slab = { + info = memcached_slab_info, + }, get = memcached_get, server = setmetatable({}, { __index = memcached_services diff --git a/memcached/internal/alloc.c b/memcached/internal/alloc.c index a1bd718..c5004f3 100644 --- a/memcached/internal/alloc.c +++ b/memcached/internal/alloc.c @@ -1,76 +1,87 @@ #include #include +#include -#include "memcached_persistent.h" +static struct slab_arena arena; +static struct slab_cache slabc; +static struct quota quota; -void * -box_persistent_malloc(size_t len, size_t *total) +struct slab_arena_info { + uint64_t quota_size; + uint64_t quota_used; + double quota_used_ratio; +}; + +struct slab_arena_info slab_info; + +/* + * Function produce an output with information of total quota size, used quota + * size and quota used ratio. Note that function relies on a fact that + * memcached uses a single slab cache for all allocations it made. + */ +struct slab_arena_info * +memcached_slab_arena_info() { - struct slab *slab = slab_get(cord_slab_cache(), len); - char *ptr = NULL; size_t sz = 0; - if (slab != NULL) { - ptr = slab_data(slab); - sz = slab_capacity(slab); - } - if (total) *total = sz; - return ptr; + /* + * How much quota has been booked - reflects the total + * size of slabs in various slab caches. + */ + slab_info.quota_used = (uint64_t)quota_used(arena.quota); + + slab_info.quota_size = (uint64_t)quota_total(arena.quota); + slab_info.quota_used_ratio = 100 * ((double) slab_info.quota_used / + ((double) slab_info.quota_size + 0.0001)); + + return &slab_info; } -void * -box_persistent_malloc0(size_t len, size_t *total) +void +memcached_slab_arena_create() { - size_t sz = 0; - char *ptr = box_persistent_malloc(len, &sz); - if (ptr) memset(ptr, 0, sz); - if (total) *total = sz; - return ptr; + static __thread bool inited = false; + if (inited) return; + size_t quota_size = QUOTA_MAX; + quota_init("a, quota_size); + /* Parameters are the same as in src/lib/core/memory.c:memory_init() */ + const size_t SLAB_SIZE = 4 * 1024 * 1024; + slab_arena_create(&arena, "a, 0, SLAB_SIZE, SLAB_ARENA_PRIVATE); + say_info("creating arena with %zu bytes...", quota_size); + inited = 1; } void -box_persistent_free(void *ptr) { - if (ptr) slab_put(cord_slab_cache(), slab_from_data(ptr)); +memcached_slab_arena_destroy() +{ + static __thread bool freed = false; + if (freed) return; + say_info("destroying arena..."); + slab_arena_destroy(&arena); + freed = 1; } -void * -box_persistent_realloc(void *ptr, size_t len, size_t *total) +void +memcached_slab_cache_create() { - if (len == 0) { - if (ptr) box_persistent_free(ptr); - if (total) *total = 0; - return NULL; - } else if (ptr == NULL) { - return box_persistent_malloc(len, total); - } - struct slab *slab = slab_from_data(ptr); - size_t sz = slab_capacity(slab); - if (len <= sz) { - if (total) *total = sz; - return ptr; - } - char *ptr_new = box_persistent_malloc(len, total); - if (!ptr_new) - return NULL; - memcpy(ptr_new, ptr, sz); - box_persistent_free(ptr); - return ptr_new; + static __thread bool inited = false; + if (inited) return; + slab_cache_set_thread(&slabc); + slab_cache_create(&slabc, &arena); + say_info("allocate slab cache with slab size %u", arena.slab_size); + inited = 1; } -void * -box_persistent_strdup(const void *str, size_t *total) +void +memcached_slab_cache_destroy() { - size_t strl = strlen(str); - char *ptr = box_persistent_malloc(strl, total); - if (ptr) { - memcpy(ptr, str, strl); - ptr[strl] = 0; - } - return ptr; + static __thread bool freed = false; + if (freed) return; + say_info("destroying slab cache"); + slab_cache_destroy(&slabc); + freed = 1; } -void * -box_persistent_strndup(const void *str, size_t len, size_t *total) +struct slab_cache * +memcached_slab_cache() { - char *ptr = box_persistent_malloc(len, total); - if (ptr) strncpy(ptr, str, len); - return ptr; + return &slabc; } diff --git a/memcached/internal/alloc.h b/memcached/internal/alloc.h index 9395d9e..efcb19e 100644 --- a/memcached/internal/alloc.h +++ b/memcached/internal/alloc.h @@ -1,22 +1,22 @@ #ifndef ALLOC_H_INCLUDED #define ALLOC_H_INCLUDED -void * -box_persistent_malloc(size_t len, size_t *total); +void +memcached_slab_arena_create(); -void * -box_persistent_malloc0(size_t len, size_t *total); +void +memcached_slab_arena_destroy(); void -box_persistent_free(void *ptr); +memcached_slab_cache_create(); -void * -box_persistent_realloc(void *ptr, size_t len, size_t *total); +void +memcached_slab_cache_destroy(); -void * -box_persistent_strdup(const void *str, size_t *total); +struct slab_cache * +memcached_slab_cache(); -void * -box_persistent_strndup(const void *str, size_t len, size_t *total); +struct slab_arena_info * +memcached_slab_arena_info(); #endif /* ALLOC_H_INCLUDED */ diff --git a/memcached/internal/memcached.c b/memcached/internal/memcached.c index f79fbd0..91e970f 100644 --- a/memcached/internal/memcached.c +++ b/memcached/internal/memcached.c @@ -37,6 +37,7 @@ #include #include +#include "alloc.h" #include "memcached.h" #include "memcached_layer.h" #include "error.h" @@ -46,6 +47,12 @@ #include "expiration.h" #include "mc_sasl.h" +__attribute__((destructor)) static void +destroy_allocations() { + memcached_slab_cache_destroy(); + memcached_slab_arena_destroy(); +} + static inline int memcached_skip_request(struct memcached_connection *con) { struct ibuf *in = con->in; @@ -250,7 +257,7 @@ memcached_handler(struct memcached_service *p, int fd) assert(0); /* unreacheable */ } - region_create(&con.gc, cord_slab_cache()); + region_create(&con.gc, memcached_slab_cache()); /* read-write cycle */ con.cfg->stat.curr_conns++; diff --git a/memcached/internal/network.c b/memcached/internal/network.c index 9e10f36..02126a6 100644 --- a/memcached/internal/network.c +++ b/memcached/internal/network.c @@ -14,6 +14,7 @@ #include #include +#include "alloc.h" #include "memcached.h" #include "constants.h" #include "network.h" @@ -31,8 +32,8 @@ iobuf_mempool_create() { static __thread bool inited = false; if (inited) return; - mempool_create(&ibuf_pool, cord_slab_cache(), sizeof(struct ibuf)); - mempool_create(&obuf_pool, cord_slab_cache(), sizeof(struct obuf)); + mempool_create(&ibuf_pool, memcached_slab_cache(), sizeof(struct ibuf)); + mempool_create(&obuf_pool, memcached_slab_cache(), sizeof(struct obuf)); inited = 1; } @@ -48,7 +49,7 @@ ibuf_new() { void *ibuf = mempool_alloc(&ibuf_pool); if (ibuf == NULL) return NULL; - ibuf_create((struct ibuf *)ibuf, cord_slab_cache(), iobuf_readahead); + ibuf_create((struct ibuf *)ibuf, memcached_slab_cache(), iobuf_readahead); return ibuf; } @@ -57,7 +58,7 @@ obuf_new() { void *obuf = mempool_alloc(&obuf_pool); if (obuf == NULL) return NULL; - obuf_create((struct obuf *)obuf, cord_slab_cache(), iobuf_readahead); + obuf_create((struct obuf *)obuf, memcached_slab_cache(), iobuf_readahead); return obuf; } diff --git a/test/common/instance_api.test.lua b/test/common/instance_api.test.lua new file mode 100755 index 0000000..2aaf166 --- /dev/null +++ b/test/common/instance_api.test.lua @@ -0,0 +1,131 @@ +#!/usr/bin/env tarantool + +local socket = require('socket') +local tap = require('tap') +package.cpath = './?.so;' .. package.cpath +local memcached = require('memcached') +local test = tap.test('memcached instance api') + +local function is_port_open(port) + local sock, _ = socket.tcp_connect('127.0.0.1', port) + if sock == nil then + return false + end + return true +end + +if type(box.cfg) == 'function' then + box.cfg{ + wal_mode = 'none', + memtx_memory = 100 * 1024 * 1024, + } + box.schema.user.grant('guest', 'read,write,execute', 'universe') +end + +test:plan(50) + +-- memcached.create() + +local mc_port = 11211 +local mc_name = 'memcached_xxx' +local mc_space_name = 'memcached_xxx_space' + +local mc = memcached.create(mc_name, tostring(mc_port), { + space_name = mc_space_name +}) +test:isnt(mc, nil, 'memcached instance object is not nil') +test:is(is_port_open(mc_port), true, 'memcached instance is started') + +-- instance:stop() + +local self_instance = mc:stop() +test:is(self_instance, mc, 'instance:stop() returns self object') +test:is(is_port_open(mc_port), false, 'port is closed when memcached instance is stopped') +local ok, err = pcall(mc.stop, mc) +test:is(ok, false, 'instance:stop() of stopped instance is failed') +test:like(err:gsub("^.+:%d+: ", ""), ("Memcached instance '%s' is already stopped"):format(mc_name), + 'instance:stop() of stopped instance shows a correct error message') + +-- instance:start() + +local self_instance = mc:start() +test:is(self_instance, mc, 'instance:start() returns self object') +test:is(is_port_open(mc_port), true, 'instance:start() opens a port') +local ok, err = pcall(mc.start, mc) +test:is(ok, false, 'instance:start() of started instance is failed') +test:like(err:gsub("^.+:%d+: ", ""), ("Memcached instance '%s' is already started"):format(mc_name), + 'instance:start() of started instance shows a correct error message') + +-- instance:info() + +local instance_info = mc:info() +test:istable(instance_info, 'type of instance:info() is a table') +test:is(instance_info.delete_misses, 0, 'default value of delete_misses is correct') +test:is(instance_info.get_hits, 0, 'default value of get_hits is correct') +test:is(instance_info.bytes_written, 0, 'default value of bytes_written is correct') +test:is(instance_info.curr_conns, 0, 'default value of curr_conns is correct') +test:is(instance_info.total_items, 0, 'default value of total_items is correct') +test:is(instance_info.evictions, 0, 'default value of evictions is correct') +test:is(instance_info.auth_errors, 0, 'default value of auth_errors is correct') +test:is(instance_info.auth_cmds, 0, 'default value of auth_cmds is correct') +test:is(instance_info.reclaimed, 0, 'default value of reclaimed is correct') +test:is(instance_info.cmd_incr, 0, 'default value of cmd_incr is correct') +test:is(instance_info.cmd_decr, 0, 'default value of cmd_decr is correct') +test:is(instance_info.cmd_touch, 0, 'default value of cmd_touch is correct') +test:is(instance_info.touch_hits, 0, 'default value of touch_hits is correct') +test:is(instance_info.touch_misses, 0, 'default value of touch_misses is correct') +test:is(instance_info.decr_misses, 0, 'default value of decr_misses is correct') +test:is(instance_info.curr_items, 0, 'default value of curr_items is correct') +test:is(instance_info.total_conns, 0, 'default value of total_conns is correct') +test:is(instance_info.incr_misses, 0, 'default value of incr_misses is correct') +test:is(instance_info.cmd_flush, 0, 'default value of cmd_flush is correct') +test:is(instance_info.decr_hits, 0, 'default value of decr_hits is correct') +test:is(instance_info.incr_hits, 0, 'default value of incr_hits is correct') +test:is(instance_info.cmd_delete, 0, 'default value of cmd_delete is correct') +test:is(instance_info.cas_misses, 0, 'default value of cas_misses is correct') +test:is(instance_info.bytes_read, 0, 'default value of bytes_read is correct') +test:is(instance_info.cas_badval, 0, 'default value of cas_badval is correct') +test:is(instance_info.delete_hits, 0, 'default value of delete_hits is correct') +test:is(instance_info.cmd_get, 0, 'default value of cmd_get is correct') +test:is(instance_info.cas_hits, 0, 'default value of cas_hits is correct') +test:is(instance_info.cmd_set, 0, 'default value of cmd_set is correct') +test:is(instance_info.get_misses, 0, 'default value of get_misses is correct') + +-- instance:cfg() + +local updated_expire_full_scan_time = mc.opts.expire_full_scan_time + 100 +local updated_expire_items_per_iter = mc.opts.expire_items_per_iter + 50 +local updated_expire_enabled = not mc.opts.expire_enabled +local updated_protocol = 'binary' +local updated_name = mc.opts.name .. '_hello' +local updated_verbosity = mc.opts.verbosity + 1 +local updated_sasl = not mc.opts.sasl + +local self_instance = mc:cfg({ + expire_full_scan_time = updated_expire_full_scan_time, + expire_items_per_iter = updated_expire_items_per_iter, + expire_enabled = updated_expire_enabled, + protocol = updated_protocol, + name = updated_name, + verbosity = updated_verbosity, + sasl = updated_sasl, +}) +test:is(self_instance, mc, 'instance:cfg() returns a self object') +test:is(is_port_open(mc_port), true, 'instance:cfg() keeps port opened') +test:is(mc.opts.expire_full_scan_time, updated_expire_full_scan_time, + 'instance:cfg() updated expire_full_scan_time correctly') +test:is(mc.opts.expire_items_per_iter, updated_expire_items_per_iter, + 'instance:cfg() updated expire_items_per_iter correctly') +test:is(mc.opts.expire_enabled, updated_expire_enabled, + 'instance:cfg() updated expire_enabled correctly') +test:is(mc.opts.protocol, updated_protocol, 'instance:cfg() updated protocol correctly') +test:is(mc.opts.verbosity, updated_verbosity, 'instance:cfg() updated verbosity correctly') +test:is(mc.opts.sasl, updated_sasl, 'instance:cfg() updated sasl correctly') + +-- instance:grant() + +box.schema.user.create('Vera') +local self_instance = mc:grant('Vera') +test:is(self_instance, mc, 'instance:grant() returns self object') + +os.exit(test:check() and 0 or 1) diff --git a/test/common/module_api.test.lua b/test/common/module_api.test.lua new file mode 100755 index 0000000..f5cacc9 --- /dev/null +++ b/test/common/module_api.test.lua @@ -0,0 +1,148 @@ +#!/usr/bin/env tarantool + +local socket = require('socket') +local tap = require('tap') +local ffi = require('ffi') +package.cpath = './?.so;' .. package.cpath +local memcached = require('memcached') +local test = tap.test('memcached module api') +local abs = require('math').abs + +-- 1. Open connection to memcached instance. +-- 2. Set a key 'key' to value 5. +-- 3. Get a value of key 'key'. +-- 4. Make sure value is equal to 5. +-- 5. Close connection. +local function check_memcached(port, timeout) + timeout = timeout or 0.5 + local sock, err = socket.tcp_connect('127.0.0.1', port, timeout) + if sock == nil then + print(err) + return false + end + sock:nonblock(true) + + -- Set a value. + local cmd = 'set key 0 60 5\r\nvalue\r\n' + local send = sock:send(cmd) + assert(send > 0) + + -- Get a value. + send = sock:send('get key\r\n') + assert(send > 0) + local read = sock:read(23) + local v = read:match('VALUE key 0 ([0-9])') + assert(v == '5') + + -- Close connection. + sock:close() + + return tonumber(v) == 5 +end + +if type(box.cfg) == 'function' then + box.cfg{ + wal_mode = 'none', + memtx_memory = 100 * 1024 * 1024, + } + box.schema.user.grant('guest', 'read,write,execute', 'universe') +end + +test:plan(31) + +-- memcached.server: module is initialized, no instances + +test:istable(memcached.server, 'type of memcached.server is a table') +test:is(#memcached.server, 0, 'memcached.server is empty') + +-- memcached.slab(): memcached module is initialized, no instances + +test:istable(memcached.slab, 'memcached.slab (no instances) returns a table') +test:isfunction(memcached.slab.info, 'memcached.slab.info() (no instances) returns a function') +local slab_info = memcached.slab.info() +test:iscdata(slab_info.quota_size, ffi.typeof('uint64_t'), + 'memcached.slab.info().quota_size (no instances) returns a cdata') +test:is(slab_info.quota_size, 4398046510080, 'memcached.slab.info().quota_size (no instances) is correct') +test:iscdata(slab_info.quota_used, ffi.typeof('uint64_t'), + 'memcached.slab.info().quota_used (no instances) returns a cdata') +test:is(slab_info.quota_used, 0, 'memcached.slab.info().quota_used (no instances) is correct') +test:isstring(slab_info.quota_used_ratio, 'memcached.slab.info().quota_used_ratio (no instances) returns a string') + +-- memcached.create(): instance 1 + +local mc_1_port = 11211 +local mc_1_name = 'memcached_1_xxx' +local mc_1_space_name = 'memcached_1_xxx_space' + +local mc_1 = memcached.create(mc_1_name, tostring(mc_1_port), { + space_name = mc_1_space_name +}) +test:isnt(mc_1, nil, '1st memcached instance object is not nil') +test:is(check_memcached(mc_1_port), true, 'connected to 1st memcached instance') +mc_1:stop() + +-- memcached.create(): instance 2 + +local mc_2_port = 11212 +local mc_2_name = 'memcached_2_xxx' +local mc_2_space_name = 'memcached_2_xxx_space' + +local mc_2 = memcached.create(mc_2_name, tostring(mc_2_port), { + space_name = mc_2_space_name +}) +test:isnt(mc_2, nil, '2nd memcached instance object is not nil') +test:is(check_memcached(mc_2_port), true, 'connected to 2nd memcached instance') +mc_2:stop() + +-- memcached.server with created and started instances + +mc_1:start() +mc_2:start() + +test:istable(memcached.server, 'type of memcached.server is a table') +test:is(#memcached.server, 0, 'memcached.server is empty') + +mc_1:stop() +mc_2:stop() + +-- memcached.get() + +test:is(memcached.get(), nil, 'memcached.get() without arguments returns nil') + +-- memcached.get(instance_1_name) + +local instance_1_info = memcached.get(mc_1_name) +test:is(instance_1_info.name, mc_1.name, 'memcached.get(): name of instance 1 is correct') +test:is(instance_1_info.space_name, mc_1.space_name, 'memcached.get(): space name of instance 1 is correct') +test:is(instance_1_info.space.id, mc_1.space.id, 'memcached.get(): space id of instance 1 is correct') + +-- memcached.get(instance_2_name) + +local instance_2_info = memcached.get(mc_2_name) +test:is(instance_2_info.name, mc_2.name, 'memcached.get(): name of instance 2 is correct') +test:is(instance_2_info.space_name, mc_2.space_name, 'memcached.get(): space name of instance 2 is correct') +test:is(instance_2_info.space.id, mc_2.space.id, 'memcached.get(): space id of instance 2 is correct') + +-- memcached.slab(): check numbers with created memcached instances + +mc_1:start() +mc_2:start() + +test:istable(memcached.slab, 'memcached.slab returns a table') +test:isfunction(memcached.slab.info, 'memcached.slab.info() returns a function') +local slab_info = memcached.slab.info() +test:istable(slab_info, 'memcached.slab.info() returns a table') +test:iscdata(slab_info.quota_size, ffi.typeof('uint64_t'), 'memcached.slab.info().quota_size returns a cdata') +test:is(slab_info.quota_size, 4398046510080, 'memcached.slab.info().quota_size is correct') +test:iscdata(slab_info.quota_used, ffi.typeof('uint64_t'), 'memcached.slab.info().quota_used returns a cdata') +test:is(slab_info.quota_used, 4194304, 'memcached.slab.info().quota_used is correct') +test:isstring(slab_info.quota_used_ratio, 'memcached.slab.info().quota_used_ratio returns a string') +local expected_ratio = tonumber(100 * slab_info.quota_used / slab_info.quota_size) +local actual_ratio = tonumber(slab_info.quota_used_ratio:match('^(%d.%d+)%%')) +local eps_is_correct = abs(actual_ratio - expected_ratio) < 1 +test:is(eps_is_correct, true, 'memcached.slab.info().quota_used_ratio is correct') + +mc_1:stop() +mc_2:stop() + +os.exit(test:check() and 0 or 1)