[PATCH 6/6] Replace schema lock with fine-grained locking
Konstantin Osipov
kostja at tarantool.org
Wed Jul 3 22:35:41 MSK 2019
* Vladimir Davydov <vdavydov.dev at gmail.com> [19/07/01 10:04]:
> Now, as we don't need to take the schema lock for checkpointing, it is
> only used to synchronize concurrent space modifications (drop, truncate,
> alter). Actually, a global lock is a way too heavy means to achieve this
> goal, because we only care about forbidding concurrent modifications of
> the same space while concurrent modifications of different spaces should
> work just fine. So this patch replaces schema lock with a per-space
> flag. The flag is called is_in_alter and set by alter_space_new() and
> cleared by alter_space_delete(). If the flag is already set when
> alter_space_new() is called, an error is thrown.
Uh-oh.
Could you please do a bit more coding?
There are inherent dangers in using a boolean flag rather than a
normal lock:
- lock life time is bound to object life time
- there is no way to put itself into a wait queue
- there is an implicit assumption that a fiber only takes one lock
and no way to inspect/free all locks of a fiber.
- deadlock detection is impossible.
Let's add a normal name-based locking for this:
struct lock {
enum object_type object_type;
char *object_name;
enum { S, X } type;
struct lock *pending;
struct fiber *owner; // ideally it should be struct txn, or int txn_id, not struct fiber
};
hash<lock> metadata_locks;
This could be a separate module in box.cc. The api should take
locks by name:
struct lock *metadata_lock_get(enum object_type type, char
*object_name, enum lock_type type, int txn_id);
and unlock by value or txn_id:
void metadata_lock_free(struct lock *lock);
void metadata_lock_free_all(int txn_id);
> Removal of the schema lock allows us to remove latch_steal() helper and
> on_begin_stmt txn trigger altogether, as they were introduced solely to
> support locking.
>
> This is a prerequisite for transactional DDL, because it's unclear how
> to preserve the global schema lock while allowing to combine several DDL
> statements in the same transaction.
> ---
> src/box/alter.cc | 68 +++++-----------------------
> src/box/alter.h | 3 --
> src/box/schema.cc | 41 +++++++----------
> src/box/schema.h | 6 ---
> src/box/space.c | 2 -
> src/box/space.h | 7 ++-
> src/box/txn.c | 3 --
> src/box/vy_lsm.c | 1 +
> src/lib/core/latch.h | 10 -----
> test/box/on_replace.result | 8 ++--
> test/box/transaction.result | 2 +-
> test/engine/errinj_ddl.result | 98 +++++++++++++++++++++++++++++++++++++++++
> test/engine/errinj_ddl.test.lua | 38 ++++++++++++++++
> 13 files changed, 176 insertions(+), 111 deletions(-)
>
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index e76b9e68..f82993c3 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -568,7 +568,6 @@ space_swap_triggers(struct space *new_space, struct space *old_space)
> {
> rlist_swap(&new_space->before_replace, &old_space->before_replace);
> rlist_swap(&new_space->on_replace, &old_space->on_replace);
> - rlist_swap(&new_space->on_stmt_begin, &old_space->on_stmt_begin);
> /** Swap SQL Triggers pointer. */
> struct sql_trigger *new_value = new_space->sql_triggers;
> new_space->sql_triggers = old_space->sql_triggers;
> @@ -707,6 +706,10 @@ struct alter_space {
> static struct alter_space *
> alter_space_new(struct space *old_space)
> {
> + if (old_space->is_in_alter) {
> + tnt_raise(ClientError, ER_ALTER_SPACE, space_name(old_space),
> + "the space is already being modified");
> + }
> struct alter_space *alter =
> region_calloc_object_xc(&in_txn()->region, struct alter_space);
> rlist_create(&alter->ops);
> @@ -716,6 +719,8 @@ alter_space_new(struct space *old_space)
> alter->new_min_field_count = old_space->format->min_field_count;
> else
> alter->new_min_field_count = 0;
> +
> + old_space->is_in_alter = true;
> return alter;
> }
>
> @@ -723,6 +728,11 @@ alter_space_new(struct space *old_space)
> static void
> alter_space_delete(struct alter_space *alter)
> {
> + if (alter->old_space != NULL) {
> + assert(alter->old_space->is_in_alter);
> + alter->old_space->is_in_alter = false;
> + }
> +
> /* Destroy the ops. */
> while (! rlist_empty(&alter->ops)) {
> AlterSpaceOp *op = rlist_shift_entry(&alter->ops,
> @@ -769,6 +779,7 @@ alter_space_commit(struct trigger *trigger, void *event)
> * going to use it.
> */
> space_delete(alter->old_space);
> + alter->old_space = NULL;
> alter_space_delete(alter);
> }
>
> @@ -3570,49 +3581,6 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
>
> /* }}} sequence */
>
> -static void
> -unlock_after_dd(struct trigger *trigger, void *event)
> -{
> - (void) trigger;
> - (void) event;
> - /*
> - * In case of yielding journal this trigger will be processed
> - * in a context of tx_prio endpoint instead of a context of
> - * a fiber which has this latch locked. So steal the latch first.
> - */
> - latch_steal(&schema_lock);
> - latch_unlock(&schema_lock);
> -}
> -
> -static void
> -lock_before_dd(struct trigger *trigger, void *event)
> -{
> - (void) trigger;
> - if (fiber() == latch_owner(&schema_lock))
> - return;
> - struct txn *txn = (struct txn *)event;
> - /*
> - * This trigger is executed before any check and may yield
> - * on the latch lock. But a yield in a non-autocommit
> - * memtx transaction will roll it back silently, rather
> - * than produce an error, which is very confusing.
> - * So don't try to lock a latch if there is
> - * a multi-statement transaction.
> - */
> - txn_check_singlestatement_xc(txn, "DDL");
> - struct trigger *on_commit =
> - txn_alter_trigger_new(unlock_after_dd, NULL);
> - struct trigger *on_rollback =
> - txn_alter_trigger_new(unlock_after_dd, NULL);
> - /*
> - * Setting triggers doesn't fail. Lock the latch last
> - * to avoid leaking the latch in case of exception.
> - */
> - txn_on_commit(txn, on_commit);
> - txn_on_rollback(txn, on_rollback);
> - latch_lock(&schema_lock);
> -}
> -
> /**
> * Trigger invoked on rollback in the _trigger space.
> * Since rollback trigger is invoked after insertion to hash and space,
> @@ -4473,18 +4441,6 @@ struct trigger on_replace_space_sequence = {
> RLIST_LINK_INITIALIZER, on_replace_dd_space_sequence, NULL, NULL
> };
>
> -struct trigger on_stmt_begin_space = {
> - RLIST_LINK_INITIALIZER, lock_before_dd, NULL, NULL
> -};
> -
> -struct trigger on_stmt_begin_index = {
> - RLIST_LINK_INITIALIZER, lock_before_dd, NULL, NULL
> -};
> -
> -struct trigger on_stmt_begin_truncate = {
> - RLIST_LINK_INITIALIZER, lock_before_dd, NULL, NULL
> -};
> -
> struct trigger on_replace_trigger = {
> RLIST_LINK_INITIALIZER, on_replace_dd_trigger, NULL, NULL
> };
> diff --git a/src/box/alter.h b/src/box/alter.h
> index b9ba7b84..c339ccea 100644
> --- a/src/box/alter.h
> +++ b/src/box/alter.h
> @@ -47,8 +47,5 @@ extern struct trigger on_replace_space_sequence;
> extern struct trigger on_replace_trigger;
> extern struct trigger on_replace_fk_constraint;
> extern struct trigger on_replace_ck_constraint;
> -extern struct trigger on_stmt_begin_space;
> -extern struct trigger on_stmt_begin_index;
> -extern struct trigger on_stmt_begin_truncate;
>
> #endif /* INCLUDES_TARANTOOL_BOX_ALTER_H */
> diff --git a/src/box/schema.cc b/src/box/schema.cc
> index 64412fac..0ab256ed 100644
> --- a/src/box/schema.cc
> +++ b/src/box/schema.cc
> @@ -37,6 +37,7 @@
> #include "scoped_guard.h"
> #include "user.h"
> #include "vclock.h"
> +#include "fiber.h"
>
> /**
> * @module Data Dictionary
> @@ -74,11 +75,6 @@ struct rlist on_alter_space = RLIST_HEAD_INITIALIZER(on_alter_space);
> struct rlist on_alter_sequence = RLIST_HEAD_INITIALIZER(on_alter_sequence);
> struct rlist on_alter_func = RLIST_HEAD_INITIALIZER(on_alter_func);
>
> -/**
> - * Lock of scheme modification
> - */
> -struct latch schema_lock = LATCH_INITIALIZER(schema_lock);
> -
> struct entity_access entity_access;
>
> bool
> @@ -268,8 +264,7 @@ static void
> sc_space_new(uint32_t id, const char *name,
> struct key_part_def *key_parts,
> uint32_t key_part_count,
> - struct trigger *replace_trigger,
> - struct trigger *stmt_begin_trigger)
> + struct trigger *replace_trigger)
> {
> struct key_def *key_def = key_def_new(key_parts, key_part_count);
> if (key_def == NULL)
> @@ -298,8 +293,6 @@ sc_space_new(uint32_t id, const char *name,
> space_cache_replace(NULL, space);
> if (replace_trigger)
> trigger_add(&space->on_replace, replace_trigger);
> - if (stmt_begin_trigger)
> - trigger_add(&space->on_stmt_begin, stmt_begin_trigger);
> /*
> * Data dictionary spaces are fully built since:
> * - they contain data right from the start
> @@ -394,56 +387,56 @@ schema_init()
> key_parts[0].fieldno = 0;
> key_parts[0].type = FIELD_TYPE_STRING;
> sc_space_new(BOX_SCHEMA_ID, "_schema", key_parts, 1,
> - &on_replace_schema, NULL);
> + &on_replace_schema);
>
> /* _collation - collation description. */
> key_parts[0].fieldno = 0;
> key_parts[0].type = FIELD_TYPE_UNSIGNED;
> sc_space_new(BOX_COLLATION_ID, "_collation", key_parts, 1,
> - &on_replace_collation, NULL);
> + &on_replace_collation);
>
> /* _space - home for all spaces. */
> sc_space_new(BOX_SPACE_ID, "_space", key_parts, 1,
> - &alter_space_on_replace_space, &on_stmt_begin_space);
> + &alter_space_on_replace_space);
>
> /* _truncate - auxiliary space for triggering space truncation. */
> sc_space_new(BOX_TRUNCATE_ID, "_truncate", key_parts, 1,
> - &on_replace_truncate, &on_stmt_begin_truncate);
> + &on_replace_truncate);
>
> /* _sequence - definition of all sequence objects. */
> sc_space_new(BOX_SEQUENCE_ID, "_sequence", key_parts, 1,
> - &on_replace_sequence, NULL);
> + &on_replace_sequence);
>
> /* _sequence_data - current sequence value. */
> sc_space_new(BOX_SEQUENCE_DATA_ID, "_sequence_data", key_parts, 1,
> - &on_replace_sequence_data, NULL);
> + &on_replace_sequence_data);
>
> /* _space_seq - association space <-> sequence. */
> sc_space_new(BOX_SPACE_SEQUENCE_ID, "_space_sequence", key_parts, 1,
> - &on_replace_space_sequence, NULL);
> + &on_replace_space_sequence);
>
> /* _user - all existing users */
> - sc_space_new(BOX_USER_ID, "_user", key_parts, 1, &on_replace_user, NULL);
> + sc_space_new(BOX_USER_ID, "_user", key_parts, 1, &on_replace_user);
>
> /* _func - all executable objects on which one can have grants */
> - sc_space_new(BOX_FUNC_ID, "_func", key_parts, 1, &on_replace_func, NULL);
> + sc_space_new(BOX_FUNC_ID, "_func", key_parts, 1, &on_replace_func);
> /*
> * _priv - association user <-> object
> * The real index is defined in the snapshot.
> */
> - sc_space_new(BOX_PRIV_ID, "_priv", key_parts, 1, &on_replace_priv, NULL);
> + sc_space_new(BOX_PRIV_ID, "_priv", key_parts, 1, &on_replace_priv);
> /*
> * _cluster - association instance uuid <-> instance id
> * The real index is defined in the snapshot.
> */
> sc_space_new(BOX_CLUSTER_ID, "_cluster", key_parts, 1,
> - &on_replace_cluster, NULL);
> + &on_replace_cluster);
>
> /* _trigger - all existing SQL triggers. */
> key_parts[0].fieldno = 0;
> key_parts[0].type = FIELD_TYPE_STRING;
> sc_space_new(BOX_TRIGGER_ID, "_trigger", key_parts, 1,
> - &on_replace_trigger, NULL);
> + &on_replace_trigger);
>
> /* _index - definition of all space indexes. */
> key_parts[0].fieldno = 0; /* space id */
> @@ -451,7 +444,7 @@ schema_init()
> key_parts[1].fieldno = 1; /* index id */
> key_parts[1].type = FIELD_TYPE_UNSIGNED;
> sc_space_new(BOX_INDEX_ID, "_index", key_parts, 2,
> - &alter_space_on_replace_index, &on_stmt_begin_index);
> + &alter_space_on_replace_index);
>
> /* _fk_сonstraint - foreign keys constraints. */
> key_parts[0].fieldno = 0; /* constraint name */
> @@ -459,7 +452,7 @@ schema_init()
> key_parts[1].fieldno = 1; /* child space */
> key_parts[1].type = FIELD_TYPE_UNSIGNED;
> sc_space_new(BOX_FK_CONSTRAINT_ID, "_fk_constraint", key_parts, 2,
> - &on_replace_fk_constraint, NULL);
> + &on_replace_fk_constraint);
>
> /* _ck_сonstraint - check constraints. */
> key_parts[0].fieldno = 0; /* space id */
> @@ -467,7 +460,7 @@ schema_init()
> key_parts[1].fieldno = 1; /* constraint name */
> key_parts[1].type = FIELD_TYPE_STRING;
> sc_space_new(BOX_CK_CONSTRAINT_ID, "_ck_constraint", key_parts, 2,
> - &on_replace_ck_constraint, NULL);
> + &on_replace_ck_constraint);
>
> /*
> * _vinyl_deferred_delete - blackhole that is needed
> diff --git a/src/box/schema.h b/src/box/schema.h
> index f0039b29..9f8f6345 100644
> --- a/src/box/schema.h
> +++ b/src/box/schema.h
> @@ -35,7 +35,6 @@
> #include <stdio.h> /* snprintf */
> #include "error.h"
> #include "space.h"
> -#include "latch.h"
>
> #if defined(__cplusplus)
> extern "C" {
> @@ -48,11 +47,6 @@ extern uint32_t space_cache_version;
> extern struct rlist on_schema_init;
>
> /**
> - * Lock of schema modification
> - */
> -extern struct latch schema_lock;
> -
> -/**
> * Try to look up a space by space number in the space cache.
> * FFI-friendly no-exception-thrown space lookup function.
> *
> diff --git a/src/box/space.c b/src/box/space.c
> index b6ad87bf..e7babb2a 100644
> --- a/src/box/space.c
> +++ b/src/box/space.c
> @@ -137,7 +137,6 @@ space_create(struct space *space, struct engine *engine,
> space->index_id_max = index_id_max;
> rlist_create(&space->before_replace);
> rlist_create(&space->on_replace);
> - rlist_create(&space->on_stmt_begin);
> space->run_triggers = true;
>
> space->format = format;
> @@ -219,7 +218,6 @@ space_delete(struct space *space)
> tuple_format_unref(space->format);
> trigger_destroy(&space->before_replace);
> trigger_destroy(&space->on_replace);
> - trigger_destroy(&space->on_stmt_begin);
> space_def_delete(space->def);
> /*
> * SQL Triggers should be deleted with
> diff --git a/src/box/space.h b/src/box/space.h
> index 949f37d4..2cc0643d 100644
> --- a/src/box/space.h
> +++ b/src/box/space.h
> @@ -154,8 +154,6 @@ struct space {
> struct rlist before_replace;
> /** Triggers fired after space_replace() -- see txn_commit_stmt(). */
> struct rlist on_replace;
> - /** Triggers fired before space statement */
> - struct rlist on_stmt_begin;
> /** SQL Trigger list. */
> struct sql_trigger *sql_triggers;
> /**
> @@ -183,6 +181,11 @@ struct space {
> /** Enable/disable triggers. */
> bool run_triggers;
> /**
> + * Set if the space is being altered and hence any other
> + * DDL request must be rejected.
> + */
> + bool is_in_alter;
> + /**
> * Space format or NULL if space does not have format
> * (sysview engine, for example).
> */
> diff --git a/src/box/txn.c b/src/box/txn.c
> index 95076773..818f405b 100644
> --- a/src/box/txn.c
> +++ b/src/box/txn.c
> @@ -242,9 +242,6 @@ txn_begin_stmt(struct txn *txn, struct space *space)
> if (space == NULL)
> return 0;
>
> - if (trigger_run(&space->on_stmt_begin, txn) != 0)
> - goto fail;
> -
> struct engine *engine = space->engine;
> if (txn_begin_in_engine(engine, txn) != 0)
> goto fail;
> diff --git a/src/box/vy_lsm.c b/src/box/vy_lsm.c
> index 3e134cc1..48590521 100644
> --- a/src/box/vy_lsm.c
> +++ b/src/box/vy_lsm.c
> @@ -38,6 +38,7 @@
> #include <small/mempool.h>
>
> #include "diag.h"
> +#include "fiber.h"
> #include "errcode.h"
> #include "histogram.h"
> #include "index_def.h"
> diff --git a/src/lib/core/latch.h b/src/lib/core/latch.h
> index 58094256..49c59cf6 100644
> --- a/src/lib/core/latch.h
> +++ b/src/lib/core/latch.h
> @@ -156,16 +156,6 @@ latch_trylock(struct latch *l)
> }
>
> /**
> - * Take a latch ownership
> - */
> -static inline void
> -latch_steal(struct latch *l)
> -{
> - assert(l->owner != NULL);
> - l->owner = fiber();
> -}
> -
> -/**
> * \copydoc box_latch_unlock
> */
> static inline void
> diff --git a/test/box/on_replace.result b/test/box/on_replace.result
> index a2f51409..b71c9878 100644
> --- a/test/box/on_replace.result
> +++ b/test/box/on_replace.result
> @@ -477,7 +477,7 @@ t = s:on_replace(function () s:create_index('sec') end, t)
> ...
> s:replace({2, 3})
> ---
> -- error: DDL does not support multi-statement transactions
> +- error: Space _index does not support multi-statement transactions
> ...
> t = s:on_replace(function () box.schema.user.create('newu') end, t)
> ---
> @@ -512,7 +512,7 @@ t = s:on_replace(function () s:drop() end, t)
> ...
> s:replace({5, 6})
> ---
> -- error: DDL does not support multi-statement transactions
> +- error: Space _index does not support multi-statement transactions
> ...
> t = s:on_replace(function () box.schema.func.create('newf') end, t)
> ---
> @@ -533,14 +533,14 @@ t = s:on_replace(function () s:rename('newname') end, t)
> ...
> s:replace({8, 9})
> ---
> -- error: DDL does not support multi-statement transactions
> +- error: Space _space does not support multi-statement transactions
> ...
> t = s:on_replace(function () s.index.pk:rename('newname') end, t)
> ---
> ...
> s:replace({9, 10})
> ---
> -- error: DDL does not support multi-statement transactions
> +- error: Space _index does not support multi-statement transactions
> ...
> s:select()
> ---
> diff --git a/test/box/transaction.result b/test/box/transaction.result
> index bc50735b..9da53e5b 100644
> --- a/test/box/transaction.result
> +++ b/test/box/transaction.result
> @@ -87,7 +87,7 @@ while f:status() ~= 'dead' do fiber.sleep(0) end;
> -- some operation involves more than one ddl spaces, so they should fail
> box.begin() box.schema.space.create('test');
> ---
> -- error: DDL does not support multi-statement transactions
> +- error: Space _space does not support multi-statement transactions
> ...
> box.rollback();
> ---
> diff --git a/test/engine/errinj_ddl.result b/test/engine/errinj_ddl.result
> index a28223a6..60bf34d9 100644
> --- a/test/engine/errinj_ddl.result
> +++ b/test/engine/errinj_ddl.result
> @@ -404,3 +404,101 @@ s:select()
> s:drop()
> ---
> ...
> +--
> +-- Concurrent DDL.
> +--
> +s1 = box.schema.space.create('test1', {engine = engine})
> +---
> +...
> +_ = s1:create_index('pk')
> +---
> +...
> +for i = 1, 5 do s1:insert{i, i} end
> +---
> +...
> +s2 = box.schema.space.create('test2', {engine = engine})
> +---
> +...
> +_ = s2:create_index('pk')
> +---
> +...
> +for i = 1, 5 do s2:insert{i, i} end
> +---
> +...
> +errinj.set("ERRINJ_BUILD_INDEX_DELAY", true)
> +---
> +- ok
> +...
> +ch = fiber.channel(1)
> +---
> +...
> +_ = fiber.create(function() s1:create_index('sk', {parts = {2, 'unsigned'}}) ch:put(true) end)
> +---
> +...
> +-- Modification of the same space must fail while an index creation
> +-- is in progress.
> +s1:format{{'a', 'unsigned'}, {'b', 'unsigned'}}
> +---
> +- error: 'Can''t modify space ''test1'': the space is already being modified'
> +...
> +s1:truncate()
> +---
> +- error: 'Can''t modify space ''test1'': the space is already being modified'
> +...
> +_ = s1:create_index('tk', {parts = {3, 'unsigned'}})
> +---
> +- error: 'Can''t modify space ''test1'': the space is already being modified'
> +...
> +s1:drop()
> +---
> +- error: 'Can''t modify space ''test1'': the space is already being modified'
> +...
> +-- Modification of another space must work though.
> +s2:format{{'a', 'unsigned'}, {'b', 'unsigned'}}
> +---
> +...
> +s2:truncate()
> +---
> +...
> +_ = s2:create_index('sk', {parts = {2, 'unsigned'}})
> +---
> +...
> +s2:drop()
> +---
> +...
> +s2 = box.schema.space.create('test2', {engine = engine})
> +---
> +...
> +_ = s2:create_index('pk')
> +---
> +...
> +s2:drop()
> +---
> +...
> +errinj.set("ERRINJ_BUILD_INDEX_DELAY", false)
> +---
> +- ok
> +...
> +ch:get()
> +---
> +- true
> +...
> +s1.index.pk:select()
> +---
> +- - [1, 1]
> + - [2, 2]
> + - [3, 3]
> + - [4, 4]
> + - [5, 5]
> +...
> +s1.index.sk:select()
> +---
> +- - [1, 1]
> + - [2, 2]
> + - [3, 3]
> + - [4, 4]
> + - [5, 5]
> +...
> +s1:drop()
> +---
> +...
> diff --git a/test/engine/errinj_ddl.test.lua b/test/engine/errinj_ddl.test.lua
> index c40eae93..207610c4 100644
> --- a/test/engine/errinj_ddl.test.lua
> +++ b/test/engine/errinj_ddl.test.lua
> @@ -188,3 +188,41 @@ ch:get()
>
> s:select()
> s:drop()
> +
> +--
> +-- Concurrent DDL.
> +--
> +s1 = box.schema.space.create('test1', {engine = engine})
> +_ = s1:create_index('pk')
> +for i = 1, 5 do s1:insert{i, i} end
> +
> +s2 = box.schema.space.create('test2', {engine = engine})
> +_ = s2:create_index('pk')
> +for i = 1, 5 do s2:insert{i, i} end
> +
> +errinj.set("ERRINJ_BUILD_INDEX_DELAY", true)
> +ch = fiber.channel(1)
> +_ = fiber.create(function() s1:create_index('sk', {parts = {2, 'unsigned'}}) ch:put(true) end)
> +
> +-- Modification of the same space must fail while an index creation
> +-- is in progress.
> +s1:format{{'a', 'unsigned'}, {'b', 'unsigned'}}
> +s1:truncate()
> +_ = s1:create_index('tk', {parts = {3, 'unsigned'}})
> +s1:drop()
> +
> +-- Modification of another space must work though.
> +s2:format{{'a', 'unsigned'}, {'b', 'unsigned'}}
> +s2:truncate()
> +_ = s2:create_index('sk', {parts = {2, 'unsigned'}})
> +s2:drop()
> +s2 = box.schema.space.create('test2', {engine = engine})
> +_ = s2:create_index('pk')
> +s2:drop()
> +
> +errinj.set("ERRINJ_BUILD_INDEX_DELAY", false)
> +ch:get()
> +
> +s1.index.pk:select()
> +s1.index.sk:select()
> +s1:drop()
> --
> 2.11.0
>
--
Konstantin Osipov, Moscow, Russia
More information about the Tarantool-patches
mailing list