Skip to content

Commit 11f2d99

Browse files
committed
box/lua: introduce box.watch and box.broadcast
Part of #6257 @TarantoolBot document Title: Document box.watch and box.broadcast `box.watch(key, func)` registers a watcher for the given key and returns a watcher handle, which can be used to unregister the watcher (by calling the `unregister` method). A key is an arbitrary string. It's possible to register more than one watcher for the same key. Note, garbage collection of a watcher handle doesnt result in unregistering the watcher so it's okay to discard the result of `box.watch` if the watcher is never going to be unregistered. `box.broadcast(key, value)` updates the value of the given key and signals all watchers registered for it. A watcher callback is first invoked unconditionally after the watcher registration. Subsequent invocations are triggered by `box.broadcast()` called on the local host. A watcher callback is passed the name of the key the watcher was subscribed to and the current key value. A watcher callback is always executed in a new fiber so it's okay to yield inside it. A watcher callback never runs in parallel with itself: if the key to which a watcher is subscribed is updated while the watcher callback is running, the callback will be invoked again with the new value as soon as it returns. `box.watch` and `box.broadcast` may be used before `box.cfg`. Example usage: ```lua -- Broadcast value 123 for key 'foo'. box.broadcast('foo', 123) -- Subscribe to updates of key 'foo'. w = box.watch('foo', function(key, value) assert(key == 'foo') -- do something with value end) -- Unregister the watcher when it's no longer needed. w:unregister() ```
1 parent e8b9dff commit 11f2d99

File tree

9 files changed

+971
-1
lines changed

9 files changed

+971
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## feature/core
2+
3+
* Introduced `box.broadcast` and `box.watch` functions to signal/watch
4+
user-defined state changes (gh-6257).

src/box/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ add_library(box STATIC
219219
lua/execute.c
220220
lua/key_def.c
221221
lua/merger.c
222+
lua/watcher.c
222223
${bin_sources})
223224

224225
if(CMAKE_BUILD_TYPE STREQUAL "Debug")

src/box/lua/init.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
#include "box/lua/execute.h"
6767
#include "box/lua/key_def.h"
6868
#include "box/lua/merger.h"
69+
#include "box/lua/watcher.h"
6970

7071
#include "mpstream/mpstream.h"
7172

@@ -478,6 +479,7 @@ box_lua_init(struct lua_State *L)
478479
box_lua_session_init(L);
479480
box_lua_xlog_init(L);
480481
box_lua_sql_init(L);
482+
box_lua_watcher_init(L);
481483
luaopen_net_box(L);
482484
lua_pop(L, 1);
483485
tarantool_lua_console_init(L);

src/box/lua/load_cfg.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,9 @@ local box_cfg_guard_whitelist = {
671671
session = true;
672672
tuple = true;
673673
runtime = true;
674-
ctl = true,
674+
ctl = true;
675+
watch = true;
676+
broadcast = true;
675677
NULL = true;
676678
};
677679

src/box/lua/watcher.c

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* SPDX-License-Identifier: BSD-2-Clause
3+
*
4+
* Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file.
5+
*/
6+
#include "box/lua/watcher.h"
7+
8+
#include <assert.h>
9+
#include <lua.h>
10+
#include <lauxlib.h>
11+
#include <stddef.h>
12+
13+
#include "box/watcher.h"
14+
#include "diag.h"
15+
#include "fiber.h"
16+
#include "core/cord_buf.h"
17+
#include "lua/msgpack.h"
18+
#include "lua/utils.h"
19+
#include "mpstream/mpstream.h"
20+
#include "small/ibuf.h"
21+
#include "trivia/util.h"
22+
23+
struct lbox_watcher {
24+
struct watcher base;
25+
/** Lua function reference. */
26+
int func_ref;
27+
};
28+
29+
/**
30+
* Watcher handle pushed as userdata to Lua so that a watcher can be
31+
* unregistered from Lua. Garbage collection of a handle doesn't lead to
32+
* watcher destruction.
33+
*/
34+
struct lbox_watcher_handle {
35+
struct lbox_watcher *watcher;
36+
};
37+
38+
static const char lbox_watcher_typename[] = "box.watcher";
39+
40+
/**
41+
* We keep a reference to each C function that is often called with lua_pcall
42+
* so as not to create a new Lua object each time we call it.
43+
*/
44+
static int lbox_watcher_run_lua_ref = LUA_NOREF;
45+
46+
/** Passed to pcall by lbox_watcher_run_f. */
47+
static int
48+
lbox_watcher_run_lua(struct lua_State *L)
49+
{
50+
struct lbox_watcher *watcher = lua_touserdata(L, 1);
51+
size_t key_len;
52+
const char *key = watcher_key(&watcher->base, &key_len);
53+
const char *data_end;
54+
const char *data = watcher_data(&watcher->base, &data_end);
55+
lua_rawgeti(L, LUA_REGISTRYINDEX, watcher->func_ref);
56+
lua_pushlstring(L, key, key_len);
57+
if (data != NULL) {
58+
luamp_decode(L, luaL_msgpack_default, &data);
59+
assert(data == data_end);
60+
(void)data_end;
61+
}
62+
lua_call(L, data != NULL ? 2 : 1, 0);
63+
return 0;
64+
}
65+
66+
/**
67+
* The callback runs a user-defined Lua function. Since the callback is invoked
68+
* in a newly created fiber, which doesn't have a Lua stack, we need to create
69+
* a temporary Lua stack for the call.
70+
*
71+
* A user-defined watcher function may throw. Even pushing arguments to the
72+
* stack may throw. So we wrap the callback in pcall to properly handle a Lua
73+
* exception.
74+
*/
75+
static void
76+
lbox_watcher_run_f(struct watcher *watcher)
77+
{
78+
/*
79+
* Create a new coro and reference it. Remove it
80+
* from tarantool_L stack, which is a) scarce
81+
* b) can be used by other triggers while this
82+
* trigger yields, so when it's time to clean
83+
* up the coro, we wouldn't know which stack position
84+
* it is on.
85+
*/
86+
void *L = luaT_newthread(tarantool_L);
87+
if (L == NULL) {
88+
diag_log();
89+
return;
90+
}
91+
int coro_ref = luaL_ref(tarantool_L, LUA_REGISTRYINDEX);
92+
lua_rawgeti(L, LUA_REGISTRYINDEX, lbox_watcher_run_lua_ref);
93+
lua_pushlightuserdata(L, watcher);
94+
if (luaT_call(L, 1, 0) != 0)
95+
diag_log();
96+
luaL_unref(tarantool_L, LUA_REGISTRYINDEX, coro_ref);
97+
}
98+
99+
/**
100+
* Releases the Lua function reference and frees the watcher.
101+
*/
102+
static void
103+
lbox_watcher_destroy_f(struct watcher *base)
104+
{
105+
struct lbox_watcher *watcher = (struct lbox_watcher *)base;
106+
luaL_unref(tarantool_L, LUA_REGISTRYINDEX, watcher->func_ref);
107+
free(watcher);
108+
}
109+
110+
static inline struct lbox_watcher_handle *
111+
lbox_check_watcher(struct lua_State *L, int idx)
112+
{
113+
return luaL_checkudata(L, idx, lbox_watcher_typename);
114+
}
115+
116+
static int
117+
lbox_watcher_tostring(struct lua_State *L)
118+
{
119+
lua_pushstring(L, lbox_watcher_typename);
120+
return 1;
121+
}
122+
123+
/**
124+
* Lua wrapper around box_watcher_unregister().
125+
*/
126+
static int
127+
lbox_watcher_unregister(struct lua_State *L)
128+
{
129+
struct lbox_watcher_handle *handle = lbox_check_watcher(L, 1);
130+
if (handle->watcher == NULL)
131+
return luaL_error(L, "Watcher is already unregistered");
132+
watcher_unregister(&handle->watcher->base);
133+
handle->watcher = NULL;
134+
return 0;
135+
}
136+
137+
/**
138+
* Lua wrapper around box_watcher_register().
139+
*/
140+
static int
141+
lbox_watch(struct lua_State *L)
142+
{
143+
/* Check arguments. */
144+
if (lua_gettop(L) != 2)
145+
return luaL_error(L, "Usage: box.watch(key, function)");
146+
size_t key_len;
147+
const char *key = luaL_checklstring(L, 1, &key_len);
148+
luaL_checktype(L, 2, LUA_TFUNCTION);
149+
150+
/* Create a watcher handle. */
151+
struct lbox_watcher_handle *handle = lua_newuserdata(
152+
L, sizeof(*handle));
153+
luaL_getmetatable(L, lbox_watcher_typename);
154+
lua_setmetatable(L, -2);
155+
lua_replace(L, 1);
156+
157+
/* Allocate and register a watcher. */
158+
struct lbox_watcher *watcher = xmalloc(sizeof(*watcher));
159+
watcher->func_ref = luaL_ref(L, LUA_REGISTRYINDEX);
160+
box_register_watcher(key, key_len, lbox_watcher_run_f,
161+
lbox_watcher_destroy_f, WATCHER_RUN_ASYNC,
162+
&watcher->base);
163+
handle->watcher = watcher;
164+
return 1;
165+
}
166+
167+
/**
168+
* Lua wrapper around box_broadcast().
169+
*/
170+
static int
171+
lbox_broadcast(struct lua_State *L)
172+
{
173+
int top = lua_gettop(L);
174+
if (top != 1 && top != 2)
175+
return luaL_error(L, "Usage: box.broadcast(key[, value])");
176+
size_t key_len;
177+
const char *key = luaL_checklstring(L, 1, &key_len);
178+
struct ibuf *ibuf = cord_ibuf_take();
179+
const char *data = NULL;
180+
const char *data_end = NULL;
181+
if (!lua_isnoneornil(L, 2)) {
182+
struct mpstream stream;
183+
mpstream_init(&stream, ibuf, ibuf_reserve_cb, ibuf_alloc_cb,
184+
luamp_error, L);
185+
luamp_encode(L, luaL_msgpack_default, &stream, 2);
186+
mpstream_flush(&stream);
187+
data = ibuf->rpos;
188+
data_end = data + ibuf_used(ibuf);
189+
}
190+
box_broadcast(key, key_len, data, data_end);
191+
cord_ibuf_put(ibuf);
192+
return 0;
193+
}
194+
195+
void
196+
box_lua_watcher_init(struct lua_State *L)
197+
{
198+
lua_pushcfunction(L, lbox_watcher_run_lua);
199+
lbox_watcher_run_lua_ref = luaL_ref(L, LUA_REGISTRYINDEX);
200+
201+
static const struct luaL_Reg lbox_watcher_meta[] = {
202+
{"__tostring", lbox_watcher_tostring},
203+
{"unregister", lbox_watcher_unregister},
204+
{NULL, NULL},
205+
};
206+
luaL_register_type(L, lbox_watcher_typename, lbox_watcher_meta);
207+
208+
lua_getfield(L, LUA_GLOBALSINDEX, "box");
209+
lua_pushstring(L, "watch");
210+
lua_pushcfunction(L, lbox_watch);
211+
lua_settable(L, -3);
212+
lua_pushstring(L, "broadcast");
213+
lua_pushcfunction(L, lbox_broadcast);
214+
lua_settable(L, -3);
215+
lua_pop(L, 1);
216+
}

src/box/lua/watcher.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* SPDX-License-Identifier: BSD-2-Clause
3+
*
4+
* Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file.
5+
*/
6+
#pragma once
7+
8+
#if defined(__cplusplus)
9+
extern "C" {
10+
#endif /* defined(__cplusplus) */
11+
12+
struct lua_State;
13+
14+
void
15+
box_lua_watcher_init(struct lua_State *L);
16+
17+
#if defined(__cplusplus)
18+
} /* extern "C" */
19+
#endif /* defined(__cplusplus) */

0 commit comments

Comments
 (0)