Merge pull request #657 from cloudwu/datasheet

Datasheet
This commit is contained in:
云风
2017-06-06 16:05:40 +08:00
committed by GitHub
6 changed files with 915 additions and 0 deletions

View File

@@ -63,6 +63,7 @@ LUA_CLIB_SKYNET = \
lua-stm.c \
lua-mysqlaux.c \
lua-debugchannel.c \
lua-datasheet.c \
\
SKYNET_SRC = skynet_main.c skynet_handle.c skynet_module.c skynet_mq.c \

372
lualib-src/lua-datasheet.c Normal file
View File

@@ -0,0 +1,372 @@
#include <lua.h>
#include <lauxlib.h>
#include <stdint.h>
#define NODECACHE "_ctable"
#define PROXYCACHE "_proxy"
#define TABLES "_ctables"
#define VALUE_NIL 0
#define VALUE_INTEGER 1
#define VALUE_REAL 2
#define VALUE_BOOLEAN 3
#define VALUE_TABLE 4
#define VALUE_STRING 5
#define VALUE_INVALID 6
#define INVALID_OFFSET 0xffffffff
struct proxy {
const char * data;
int index;
};
struct document {
uint32_t strtbl;
uint32_t n;
uint32_t index[1];
// table[n]
// strings
};
struct table {
uint32_t array;
uint32_t dict;
uint8_t type[1];
// value[array]
// kvpair[dict]
};
static inline const struct table *
gettable(const struct document *doc, int index) {
if (doc->index[index] == INVALID_OFFSET) {
return NULL;
}
return (const struct table *)((const char *)doc + sizeof(uint32_t) + sizeof(uint32_t) + doc->n * sizeof(uint32_t) + doc->index[index]);
}
static void
create_proxy(lua_State *L, const void *data, int index) {
const struct table * t = gettable(data, index);
if (t == NULL) {
luaL_error(L, "Invalid index %d", index);
}
lua_getfield(L, LUA_REGISTRYINDEX, NODECACHE);
if (lua_rawgetp(L, -1, t) == LUA_TTABLE) {
lua_replace(L, -2);
return;
}
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, lua_upvalueindex(1));
lua_setmetatable(L, -2);
lua_pushvalue(L, -1);
// NODECACHE, table, table
lua_rawsetp(L, -3, t);
// NODECACHE, table
lua_getfield(L, LUA_REGISTRYINDEX, PROXYCACHE);
// NODECACHE, table, PROXYCACHE
lua_pushvalue(L, -2);
// NODECACHE, table, PROXYCACHE, table
struct proxy * p = lua_newuserdata(L, sizeof(struct proxy));
// NODECACHE, table, PROXYCACHE, table, proxy
p->data = data;
p->index = index;
lua_rawset(L, -3);
// NODECACHE, table, PROXYCACHE
lua_pop(L, 1);
// NODECACHE, table
lua_replace(L, -2);
// table
}
static void
clear_table(lua_State *L) {
int t = lua_gettop(L); // clear top table
if (lua_type(L, t) != LUA_TTABLE) {
luaL_error(L, "Invalid cache");
}
lua_pushnil(L);
while (lua_next(L, t) != 0) {
// key value
lua_pop(L, 1);
lua_pushvalue(L, -1);
lua_pushnil(L);
// key key nil
lua_rawset(L, t);
// key
}
}
static void
update_cache(lua_State *L, const void *data, const void * newdata) {
lua_getfield(L, LUA_REGISTRYINDEX, NODECACHE);
int t = lua_gettop(L);
lua_getfield(L, LUA_REGISTRYINDEX, PROXYCACHE);
int pt = t + 1;
lua_newtable(L); // temp table
int nt = pt + 1;
lua_pushnil(L);
while (lua_next(L, t) != 0) {
// pointer (-2) -> table (-1)
lua_pushvalue(L, -1);
if (lua_rawget(L, pt) == LUA_TUSERDATA) {
// pointer, table, proxy
struct proxy * p = lua_touserdata(L, -1);
if (p->data == data) {
// update to newdata
p->data = newdata;
const struct table * newt = gettable(newdata, p->index);
lua_pop(L, 1);
// pointer, table
clear_table(L);
lua_pushvalue(L, lua_upvalueindex(1));
// pointer, table, meta
lua_setmetatable(L, -2);
// pointer, table
if (newt) {
lua_rawsetp(L, nt, newt);
} else {
lua_pop(L, 1);
}
// pointer
lua_pushvalue(L, -1);
lua_pushnil(L);
lua_rawset(L, t);
} else {
lua_pop(L, 2);
}
} else {
lua_pop(L, 2);
// pointer
}
}
// copy nt to t
lua_pushnil(L);
while (lua_next(L, nt) != 0) {
lua_pushvalue(L, -2);
lua_insert(L, -2);
// key key value
lua_rawset(L, t);
}
// NODECACHE PROXYCACHE TEMP
lua_pop(L, 3);
}
static int
lupdate(lua_State *L) {
lua_getfield(L, LUA_REGISTRYINDEX, PROXYCACHE);
lua_pushvalue(L, 1);
// PROXYCACHE, table
if (lua_rawget(L, -2) != LUA_TUSERDATA) {
luaL_error(L, "Invalid proxy table %p", lua_topointer(L, 1));
}
struct proxy * p = lua_touserdata(L, -1);
luaL_checktype(L, 2, LUA_TLIGHTUSERDATA);
const char * newdata = lua_touserdata(L, 2);
update_cache(L, p->data, newdata);
return 1;
}
static inline uint32_t
getuint32(const void *v) {
union {
uint32_t d;
uint8_t t[4];
} test = { 1 };
if (test.t[0] == 0) {
// big endian
test.d = *(const uint32_t *)v;
return test.t[0] | test.t[1] << 4 | test.t[2] << 8 | test.t[3] << 12;
} else {
return *(const uint32_t *)v;
}
}
static inline float
getfloat(const void *v) {
union {
uint32_t d;
float f;
uint8_t t[4];
} test = { 1 };
if (test.t[0] == 0) {
// big endian
test.d = *(const uint32_t *)v;
test.d = test.t[0] | test.t[1] << 4 | test.t[2] << 8 | test.t[3] << 12;
return test.f;
} else {
return *(const float *)v;
}
}
static void
pushvalue(lua_State *L, const void *v, int type, const struct document * doc) {
switch (type) {
case VALUE_NIL:
lua_pushnil(L);
break;
case VALUE_INTEGER:
lua_pushinteger(L, (int32_t)getuint32(v));
break;
case VALUE_REAL:
lua_pushnumber(L, getfloat(v));
break;
case VALUE_BOOLEAN:
lua_pushboolean(L, getuint32(v));
break;
case VALUE_TABLE:
create_proxy(L, doc, getuint32(v));
break;
case VALUE_STRING:
lua_pushstring(L, (const char *)doc + doc->strtbl + getuint32(v));
break;
default:
luaL_error(L, "Invalid type %d at %p", type, v);
}
}
static void
copytable(lua_State *L, int tbl, struct proxy *p) {
const struct document * doc = (const struct document *)p->data;
if (p->index < 0 || p->index >= doc->n) {
luaL_error(L, "Invalid proxy (index = %d, total = %d)", p->index, (int)doc->n);
}
const struct table * t = gettable(doc, p->index);
if (t == NULL) {
luaL_error(L, "Invalid proxy (index = %d)", p->index);
}
const uint32_t * v = (const uint32_t *)((const char *)t + sizeof(uint32_t) + sizeof(uint32_t) + ((t->array + t->dict + 3) & ~3));
int i;
for (i=0;i<t->array;i++) {
pushvalue(L, v++, t->type[i], doc);
lua_rawseti(L, tbl, i+1);
}
for (i=0;i<t->dict;i++) {
pushvalue(L, v++, VALUE_STRING, doc);
pushvalue(L, v++, t->type[t->array+i], doc);
lua_rawset(L, tbl);
}
}
static int
lnew(lua_State *L) {
luaL_checktype(L, 1, LUA_TLIGHTUSERDATA);
const char * data = lua_touserdata(L, 1);
// hold ref to data
lua_getfield(L, LUA_REGISTRYINDEX, TABLES);
lua_pushvalue(L, 1);
lua_rawsetp(L, -2, data);
create_proxy(L, data, 0);
return 1;
}
static void
copyfromdata(lua_State *L) {
lua_getfield(L, LUA_REGISTRYINDEX, PROXYCACHE);
lua_pushvalue(L, 1);
// PROXYCACHE, table
if (lua_rawget(L, -2) != LUA_TUSERDATA) {
luaL_error(L, "Invalid proxy table %p", lua_topointer(L, 1));
}
struct proxy * p = lua_touserdata(L, -1);
lua_pop(L, 2);
copytable(L, 1, p);
lua_pushnil(L);
lua_setmetatable(L, 1); // remove metatable
}
static int
lindex(lua_State *L) {
copyfromdata(L);
lua_rawget(L, 1);
return 1;
}
static int
lnext(lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
if (lua_next(L, 1))
return 2;
else {
lua_pushnil(L);
return 1;
}
}
static int
lpairs(lua_State *L) {
copyfromdata(L);
lua_pushcfunction(L, lnext);
lua_pushvalue(L, 1);
lua_pushnil(L);
return 3;
}
static int
llen(lua_State *L) {
copyfromdata(L);
lua_pushinteger(L, lua_rawlen(L, 1));
return 1;
}
static void
new_weak_table(lua_State *L, const char *mode) {
lua_newtable(L); // NODECACHE { pointer:table }
lua_createtable(L, 0, 1); // weak meta table
lua_pushstring(L, mode);
lua_setfield(L, -2, "__mode");
lua_setmetatable(L, -2); // make NODECACHE weak
}
static void
gen_metatable(lua_State *L) {
new_weak_table(L, "kv"); // NODECACHE { pointer:table }
lua_setfield(L, LUA_REGISTRYINDEX, NODECACHE);
new_weak_table(L, "k"); // PROXYCACHE { table:userdata }
lua_setfield(L, LUA_REGISTRYINDEX, PROXYCACHE);
lua_newtable(L);
lua_setfield(L, LUA_REGISTRYINDEX, TABLES);
lua_createtable(L, 0, 1); // mod table
lua_createtable(L, 0, 2); // metatable
luaL_Reg l[] = {
{ "__index", lindex },
{ "__pairs", lpairs },
{ "__len", llen },
{ NULL, NULL },
};
lua_pushvalue(L, -1);
luaL_setfuncs(L, l, 1);
}
static int
lstringpointer(lua_State *L) {
const char * str = luaL_checkstring(L, 1);
lua_pushlightuserdata(L, (void *)str);
return 1;
}
LUAMOD_API int
luaopen_skynet_datasheet_core(lua_State *L) {
luaL_checkversion(L);
luaL_Reg l[] = {
{ "new", lnew },
{ "update", lupdate },
{ NULL, NULL },
};
luaL_newlibtable(L,l);
gen_metatable(L);
luaL_setfuncs(L, l, 1);
lua_pushcfunction(L, lstringpointer);
lua_setfield(L, -2, "stringpointer");
return 1;
}

View File

@@ -0,0 +1,167 @@
local skynet = require "skynet"
local dump = require "skynet.datasheet.dump"
local core = require "skynet.datasheet.core"
local service = require "skynet.service"
local builder = {}
local cache = {}
local dataset = {}
local function monitor(pointer)
skynet.fork(function()
skynet.call(service.address, "lua", "collect", pointer)
for k,v in pairs(cache) do
if v == pointer then
cache[k] = nil
return
end
end
end)
end
local function dumpsheet(v)
if type(v) == "string" then
return v
else
return dump.dump(v)
end
end
function builder.new(name, v)
assert(dataset[name] == nil)
local datastring = dumpsheet(v)
local pointer = core.stringpointer(datastring)
skynet.call(service.address, "lua", "update", name, pointer)
cache[datastring] = pointer
dataset[name] = datastring
monitor(pointer)
end
function builder.update(name, v)
local lastversion = assert(dataset[name])
local newversion = dumpsheet(v)
local diff = dump.diff(lastversion, newversion)
local pointer = core.stringpointer(diff)
skynet.call(service.address, "lua", "update", name, pointer)
cache[diff] = pointer
local lp = assert(cache[lastversion])
skynet.send(service.address, "lua", "release", lp)
dataset[name] = diff
monitor(pointer)
end
function builder.compile(v)
return dump.dump(v)
end
local function datasheet_service()
local skynet = require "skynet"
local datasheet = {}
local handles = {} -- handle:{ ref:count , name:name , collect:resp }
local dataset = {} -- name:{ handle:handle, monitor:{monitors queue} }
local function releasehandle(handle)
local h = handles[handle]
h.ref = h.ref - 1
if h.ref == 0 and h.collect then
h.collect(true)
h.collect = nil
handles[handle] = nil
end
end
-- from builder, create or update handle
function datasheet.update(name, handle)
local t = dataset[name]
if not t then
-- new datasheet
t = { handle = handle, monitor = {} }
dataset[name] = t
handles[handle] = { ref = 1, name = name }
else
t.handle = handle
-- report update to customers
handles[handle] = { ref = 1 + #t.monitor, name = name }
for k,v in ipairs(t.monitor) do
v(true, handle)
t.monitor[k] = nil
end
end
skynet.ret()
end
-- from customers
function datasheet.query(name)
local t = assert(dataset[name], "create data first")
local handle = t.handle
local h = handles[handle]
h.ref = h.ref + 1
skynet.ret(skynet.pack(handle))
end
-- from customers, monitor handle change
function datasheet.monitor(handle)
local h = assert(handles[handle], "Invalid data handle")
local t = dataset[h.name]
if t.handle ~= handle then -- already changes
skynet.ret(skynet.pack(t.handle))
else
h.ref = h.ref + 1
table.insert(t.monitor, skynet.response())
end
end
-- from customers, release handle , ref count - 1
function datasheet.release(handle)
-- send message, don't ret
releasehandle(handle)
end
-- from builder, monitor handle release
function datasheet.collect(handle)
local h = assert(handles[handle], "Invalid data handle")
if h.ref == 0 then
handles[handle] = nil
skynet.ret()
else
assert(h.collect == nil, "Only one collect allows")
h.collect = skynet.response()
end
end
skynet.dispatch("lua", function(_,_,cmd,...)
datasheet[cmd](...)
end)
skynet.info_func(function()
local info = {}
local tmp = {}
for k,v in pairs(handles) do
tmp[k] = v
end
for k,v in pairs(dataset) do
local h = handles[v.handle]
tmp[v.handle] = nil
info[k] = {
handle = v.handle,
monitors = #v.monitor,
}
end
for k,v in pairs(tmp) do
info[k] = v.ref
end
return info
end)
end
skynet.init(function()
service.new("datasheet", datasheet_service)
end)
return builder

View File

@@ -0,0 +1,270 @@
--[[ file format
document :
int32 strtbloffset
int32 n
int32*n index table
table*n
strings
table:
int32 array
int32 dict
int8*(array+dict) type (align 4)
value*array
kvpair*dict
kvpair:
string k
value v
value: (union)
int32 integer
float real
int32 boolean
int32 table index
int32 string offset
type: (enum)
0 nil
1 integer
2 real
3 boolean
4 table
5 string
]]
local ctd = {}
local math = math
local table = table
local string = string
function ctd.dump(root)
local doc = {
table_n = 0,
table = {},
strings = {},
offset = 0,
}
local function dump_table(t)
local index = doc.table_n + 1
doc.table_n = index
doc.table[index] = false -- place holder
local array_n = 0
local array = {}
local kvs = {}
local types = {}
local function encode(v)
local t = type(v)
if t == "table" then
local index = dump_table(v)
return '\4', string.pack("<i4", index-1)
elseif t == "number" then
if math.tointeger(v) then
return '\1', string.pack("<i4", v)
else
return '\2', string.pack("<f",v)
end
elseif t == "boolean" then
if v then
return '\3', "\0\0\0\1"
else
return '\3', "\0\0\0\0"
end
elseif t == "string" then
local offset = doc.strings[v]
if not offset then
offset = doc.offset
doc.offset = offset + #v + 1
doc.strings[v] = offset
table.insert(doc.strings, v)
end
return '\5', string.pack("<I4", offset)
else
error ("Unsupport value " .. tostring(v))
end
end
for i,v in ipairs(t) do
types[i], array[i] = encode(v)
array_n = i
end
for k,v in pairs(t) do
if type(k) == "string" then
local _, kv = encode(k)
local tv, ev = encode(v)
table.insert(types, tv)
table.insert(kvs, kv .. ev)
else
local ik = math.tointeger(k)
assert(ik and ik > 0 and ik <= array_n)
end
end
-- encode table
local typeset = table.concat(types)
local align = string.rep("\0", (4 - #typeset & 3) & 3)
local tmp = {
string.pack("<i4i4", array_n, #kvs),
typeset,
align,
table.concat(array),
table.concat(kvs),
}
doc.table[index] = table.concat(tmp)
return index
end
dump_table(root)
-- encode document
local index = {}
local offset = 0
for i, v in ipairs(doc.table) do
index[i] = string.pack("<I4", offset)
offset = offset + #v
end
local tmp = {
string.pack("<I4", 4 + 4 + 4 * doc.table_n + offset),
string.pack("<I4", doc.table_n),
table.concat(index),
table.concat(doc.table),
table.concat(doc.strings, "\0")
}
return table.concat(tmp)
end
function ctd.undump(v)
local stringtbl, n = string.unpack("<I4I4",v)
local index = { string.unpack("<" .. string.rep("I4", n), v, 9) }
local header = 4 + 4 + 4 * n + 1
stringtbl = stringtbl + 1
local tblidx = {}
local function decode(n)
local toffset = index[n+1] + header
local array, dict = string.unpack("<I4I4", v, toffset)
local types = { string.unpack(string.rep("B", (array+dict)), v, toffset + 8) }
local offset = ((array + dict + 8 + 3) & ~3) + toffset
local result = {}
local function value(t)
local off = offset
offset = offset + 4
if t == 1 then -- integer
return (string.unpack("<i4", v, off))
elseif t == 2 then -- float
return (string.unpack("<f", v, off))
elseif t == 3 then -- boolean
return string.unpack("<i4", v, off) ~= 0
elseif t == 4 then -- table
local tindex = (string.unpack("<I4", v, off))
return decode(tindex)
elseif t == 5 then -- string
local sindex = string.unpack("<I4", v, off)
return (string.unpack("z", v, stringtbl + sindex))
else
error (string.format("Invalid data at %d (%d)", off, t))
end
end
for i=1,array do
table.insert(result, value(types[i]))
end
for i=1,dict do
local sindex = string.unpack("<I4", v, offset)
offset = offset + 4
local key = string.unpack("z", v, stringtbl + sindex)
result[key] = value(types[array + i])
end
tblidx[result] = n
return result
end
return decode(0), tblidx
end
local function diffmap(last, current)
local lastv, lasti = ctd.undump(last)
local curv, curi = ctd.undump(current)
local map = {} -- new(current index):old(last index)
local function comp(lastr, curr)
local old = lasti[lastr]
local new = curi[curr]
map[new] = old
for k,v in pairs(lastr) do
if type(v) == "table" then
local newv = curr[k]
if type(newv) == "table" then
comp(v, newv)
end
end
end
end
comp(lastv, curv)
return map
end
function ctd.diff(last, current)
local map = diffmap(last, current)
local stringtbl, n = string.unpack("<I4I4",current)
local _, lastn = string.unpack("<I4I4",last)
local existn = 0
for k,v in pairs(map) do
existn = existn + 1
end
local newn = lastn
for i = 0, n-1 do
if not map[i] then
map[i] = newn
newn = newn + 1
end
end
-- remap current
local index = { string.unpack("<" .. string.rep("I4", n), current, 9) }
local header = 4 + 4 + 4 * n + 1
local function remap(n)
local toffset = index[n+1] + header
local array, dict = string.unpack("<I4I4", current, toffset)
local types = { string.unpack(string.rep("B", (array+dict)), current, toffset + 8) }
local hlen = (array + dict + 8 + 3) & ~3
local hastable = false
for _, v in ipairs(types) do
if v == 4 then -- table
hastable = true
break
end
end
if not hastable then
return string.sub(current, toffset, toffset + hlen + (array + dict * 2) * 4 - 1)
end
local offset = hlen + toffset
local pat = "<" .. string.rep("I4", array + dict * 2)
local values = { string.unpack(pat, current, offset) }
for i = 1, array do
if types[i] == 4 then -- table
values[i] = map[values[i]]
end
end
for i = 1, dict do
if types[i + array] == 4 then -- table
values[array + i * 2] = map[values[array + i * 2]]
end
end
return string.sub(current, toffset, toffset + hlen - 1) ..
string.pack(pat, table.unpack(values))
end
-- rebuild
local oldindex = { string.unpack("<" .. string.rep("I4", n), current, 9) }
local index = {}
for i = 1, newn do
index[i] = 0xffffffff
end
for i = 0, #map do
index[map[i]+1] = oldindex[i+1]
end
local tmp = {
string.pack("<I4I4", stringtbl + (newn - n) * 4, newn), -- expand index table
string.pack("<" .. string.rep("I4", newn), table.unpack(index)),
}
for i = 0, n - 1 do
table.insert(tmp, remap(i))
end
table.insert(tmp, string.sub(current, stringtbl+1))
return table.concat(tmp)
end
return ctd

View File

@@ -0,0 +1,77 @@
local skynet = require "skynet"
local service = require "skynet.service"
local core = require "skynet.datasheet.core"
local datasheet_svr
skynet.init(function()
datasheet_svr = service.query "datasheet"
end)
local datasheet = {}
local sheets = setmetatable({}, {
__gc = function(t)
for _,v in pairs(t) do
skynet.send(datasheet_svr, "lua", "release", v.handle)
end
end,
})
local function querysheet(name)
return skynet.call(datasheet_svr, "lua", "query", name)
end
local function updateobject(name)
local t = sheets[name]
if not t.object then
t.object = core.new(t.handle)
end
local function monitor()
local handle = t.handle
local newhandle = skynet.call(datasheet_svr, "lua", "monitor", handle)
core.update(t.object, newhandle)
t.handle = newhandle
skynet.send(datasheet_svr, "lua", "release", handle)
skynet.fork(monitor)
end
skynet.fork(monitor)
end
function datasheet.query(name)
local t = sheets[name]
if not t then
t = {}
sheets[name] = t
end
if t.error then
error(t.error)
end
if t.object then
return t.object
end
if t.queue then
local co = coroutine.running()
table.insert(t.queue, co)
skynet.wait(co)
else
t.queue = {} -- create wait queue for other query
local ok, handle = pcall(querysheet, name)
if ok then
t.handle = handle
updateobject(name)
else
t.error = handle
end
local q = t.queue
t.queue = nil
for _, co in ipairs(q) do
skynet.wakeup(co)
end
end
if t.error then
error(t.error)
end
return t.object
end
return datasheet

28
test/testdatasheet.lua Normal file
View File

@@ -0,0 +1,28 @@
local skynet = require "skynet"
local builder = require "skynet.datasheet.builder"
local datasheet = require "skynet.datasheet"
local function dump(t, prefix)
for k,v in pairs(t) do
print(prefix, k, v)
if type(v) == "table" then
dump(v, prefix .. "." .. k)
end
end
end
skynet.start(function()
builder.new("foobar", {a = 1, b = 2 , c = {3} })
local t = datasheet.query "foobar"
local c = t.c
dump(t, "[1]")
builder.update("foobar", { b = 4, c = { 5 } })
print("sleep")
skynet.sleep(100)
dump(t, "[2]")
dump(c, "[2.c]")
builder.update("foobar", { a = 6, c = 7, d = 8 })
print("sleep")
skynet.sleep(100)
dump(t, "[3]")
end)