diff --git a/profile17.lua b/profile17.lua new file mode 100644 index 0000000..313cb81 --- /dev/null +++ b/profile17.lua @@ -0,0 +1,207 @@ +-- Tuple of profile has always 2 fields. +-- Field 1 -- user-id +-- Field 2 -- key/value dict. Key is always a number. + +proscribe_host = '10.161.41.111' +proscribe_port = 27017 +keys_to_proscribe = {324} + +-- Create a user, then init storage, create functions and then revoke ALL priveleges from user +local function init_storage(init_func, interface) + local init_username = 'profile' + box.schema.user.create(init_username, {if_not_exists = true}) + box.schema.user.grant(init_username, 'execute,read,write', 'universe', nil, + {if_not_exists = true}) + box.session.su(init_username) + + init_func() + + for _, v in pairs(interface) do + box.schema.func.create(v, {setuid = true, if_not_exists = true}) + end + + box.session.su('admin') + box.schema.user.revoke(init_username, 'execute,read,write', 'universe') +end + +-- Create a role, which can execute interface functions +local function init_role(role_name, interface) + box.schema.role.create(role_name, {if_not_exists = true}) + for _, v in pairs(interface) do + box.schema.role.grant(role_name, 'execute', 'function', v, + {if_not_exists = true}) + end +end + +-- Scheme initialization +local function init() + local s = box.schema.create_space('profile', { + if_not_exists = true, + }) + s:create_index('primary', { + type = 'tree', + unique = true, + parts = {1, 'unsigned'}, + if_not_exists = true, + }) +end + +-- List of functions, which is possible to call from outside the box +local interface = { + 'profile_delete', + 'profile_get_all', + 'profile_multiget', + 'profile_multiset', + 'profile_set', +} + +msgpack = require('msgpack') +socket = require('socket') +digest = require('digest') +pickle = require('pickle') + +proscribe_socket = socket('AF_INET', 'SOCK_DGRAM', 'udp') + +-- Function, which sends requested profile_key changes to special DWH daemon +local function send_to_proscribe(uid, profile_key, old_value, new_value) + local fit = false + for _, v in ipairs(keys_to_proscribe) do + if v == profile_key then + fit = true + end + end + if not fit then + return + end + + if not old_value then + old_value = '' + end + if not new_value then + new_value = '' + end + if old_value == new_value then -- we doesnt want to send data without changes + return + end + + old_value = digest.base64_encode(old_value) + new_value = digest.base64_encode(new_value) + + local body = pickle.pack('iliiAiA', 1, uid, profile_key, #old_value, old_value, #new_value, new_value) + local packet = pickle.pack('iiiA', 1, #body, 0, body) + proscribe_socket:sendto(proscribe_host, proscribe_port, packet) +end + +-- Cast internal profile format to the format, requested by client-side. +local function cast_profile_to_return_format(profile) + setmetatable(profile.data, msgpack.map_mt) + return {profile.uid, profile.data} +end + +local function store_profile(profile) + local count = 0 + for k,v in pairs(profile.data) do count = count + 1 end + + -- We dont want to store empty profiles. Save space. + if count == 0 then + return box.space.profile:delete(profile.uid) + end + + setmetatable(profile.data, msgpack.map_mt) + return box.space.profile:replace({profile.uid, profile.data}) +end + +local function create_new_profile(user_id) + local profile = {} + profile.uid = user_id + profile.data = {} + return profile +end + +local function load_profile(user_id) + local tup = box.space.profile:select(user_id) + -- In case no profile found, operate it as profile without keys/values + if #tup == 0 then + return create_new_profile(user_id) + end + local profile = {} + profile.uid = user_id + profile.data = tup[1][2] -- Index 1 is because we have only 1 tuple with such userid (index is unique). Second field of tuple is key/value dict. + return profile +end + +local function set_profile_key(profile, key, value) + -- Do not store empty keys. We want to save space. + if value == '' then + value = nil + end + profile.data[key] = value +end + +-- function profile_delete delete profile. Returns nothing +function profile_delete(user_id) + box.space.profile:delete(user_id) +end + +-- function profile_get_all returns full profile +function profile_get_all(user_id) + local profile = load_profile(user_id) + return cast_profile_to_return_format(profile) +end + +-- function profile_multiget returns only requested keys from profile. Accepts user_id and then several keys +function profile_multiget(user_id, ...) + local pref_list = {...} + if #pref_list == 0 then + return cast_profile_to_return_format(create_new_profile(user_id)) + end + + local profile = load_profile(user_id) + + -- Create a copy of profile. We select few keys, so it is faster to copy only needed keys, then clear not needed keys + local profile_copy = create_new_profile(profile.uid) + for _, v in ipairs(pref_list) do + if profile.data[v] then + profile_copy.data[v] = profile.data[v] + end + end + + return cast_profile_to_return_format(profile_copy) +end + +-- function profile_multiset accepts user_id and then key, value, key, value, key, value, ... Returns full updated profile. +function profile_multiset(user_id, ...) + local pref_list = {...} + + if #pref_list % 2 ~= 0 then + error('Not even number of arguments') + end + + local profile = load_profile(user_id) + + -- In case of no keys were passed, just return full profile + if #pref_list == 0 then + return cast_profile_to_return_format(profile) + end + + local i, pref_key = next(pref_list) + local i, pref_value = next(pref_list, i) + -- iterate all passed key/value pairs from arguments + while pref_key ~= nil and pref_value ~= nil do + send_to_proscribe(profile.uid, pref_key, profile.data[pref_key], pref_value) + set_profile_key(profile, pref_key, pref_value) + i, pref_key = next(pref_list, i) + i, pref_value = next(pref_list, i) + end + + store_profile(profile) + return cast_profile_to_return_format(profile) +end + +-- function profile_set set only one key. Returns full updated profile +function profile_set(user_id, key, value) + return profile_multiset(user_id, key, value) +end + +init_storage(init, interface) +init_role('profile_role', interface) diff --git a/profiles.lua b/profiles.lua index 378b343..2f16fdd 100644 --- a/profiles.lua +++ b/profiles.lua @@ -502,3 +502,69 @@ function profile_print_bigger_than(size_in_bytes) end end end + +local helper_key_id = 1011 +local email_key_id = 314 + +local function get_helpers(helpers_string) + if (helpers_string:len() - 1) % 4 ~= 0 then + return nil, "Bad size of helpers" + end + + local helpers = {} + local i = 1 + while i < helpers_string:len() do + local index = box.unpack('i', string.sub(helpers_string, i, i + 3)) + local state = bit.band(index, 0x80000000) + index = bit.band(index, 0x7FFFFFFF) + i = i + 4 + + local time = box.unpack('i', string.sub(helpers_string, i, i + 3)) + i = i + 4 + + local show = box.unpack('i', string.sub(helpers_string, i, i + 3)) + i = i + 4 + + local close = box.unpack('i', string.sub(helpers_string, i, i + 3)) + i = i + 4 + + local helper = {index = index, state = state, time = time, show = show, close = close} + + helpers[index] = helper + end + + if string.sub(helpers_string, i, i+1) ~= '!' then + return nil, "Non '!' end of helpers, invalid helpers" + end + + return helpers, nil +end + +function profile_print_with_helper(helper_number) + if box.cfg.replication_source == nil then error("replica api only") end + if type(helper_number) == "string" then helper_number = tonumber(helper_number) end + + profile_apply_func( + function(p, helper_number) + local packed_key_id = box.pack("w", helper_key_id) + local packed_email_key_id = box.pack("w", email_key_id) + + if p.prefs[packed_key_id] then + if p.prefs[packed_key_id]:len() > 1 then + local helpers, err = get_helpers(p.prefs[packed_key_id]) + + if helpers then + if helpers[helper_number] then + print("user_id: ", profile_id_to_int(p.id), " id: ", helper_key_id, " val: ", helper_number, " ", helpers[helper_number]['state'], " ", helpers[helper_number]['time'], " ", helpers[helper_number]['show'], " ", helpers[helper_number]['close'], " ", p.prefs[packed_email_key_id]) + end + else + print("user_id: ", profile_id_to_int(p.id), " id: ", helper_key_id, " val: BAD (", err, ")") + end + end + end + end, + helper_number + ) + +end + diff --git a/ussender.lua b/ussender.lua index d3c8502..e4a901f 100644 --- a/ussender.lua +++ b/ussender.lua @@ -1,12 +1,29 @@ -- Race conditions in this files are possible. It is ok by biz logic. +-- + +local tuple_length_limit = 1001 --max numbers of element + 1. VALUE MUST BE ODD. +local space_no = 0 + +local function truncate_tuple(selected) + selected = selected:transform(1, #selected - tuple_length_limit):transform(1, (tuple_length_limit - 1) / 2) + box.replace(space_no, selected) + return selected +end + function ussender_add(user_id, sender_id) local user_id = box.unpack("i", user_id) local sender_id = box.unpack("l", sender_id) - local selected = { box.select_limit(0, 0, 0, 1, user_id) } + local selected = { box.select_limit(space_no, 0, 0, 1, user_id) } + if #selected == 0 then - box.insert(0, user_id, sender_id) + box.insert(space_no, user_id, sender_id) else + + if #selected[1] >= tuple_length_limit then + selected[1] = truncate_tuple(selected[1]) + end + local fun, param, state = selected[1]:pairs() state, _ = fun(param, state) -- skip the first element of tuple for state, v in fun, param, state do @@ -15,21 +32,24 @@ function ussender_add(user_id, sender_id) return end end - box.update(0, user_id, "!p", -1, sender_id) + box.update(space_no, user_id, "!p", -1, sender_id) end end function ussender_select(user_id) local user_id = box.unpack("i", user_id) - local ret = {box.select_limit(0, 0, 0, 1, user_id)} + local ret = {box.select_limit(space_no, 0, 0, 1, user_id)} if #ret == 0 then return {user_id} end + if #ret[1] >= tuple_length_limit then + ret[1] = truncate_tuple(ret[1]) + end return ret end function ussender_delete(user_id) local user_id = box.unpack("i", user_id) - box.delete(0, user_id) + box.delete(space_no, user_id) end