Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement batched messages for Digiline Chests #87

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/chest.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The message is sent when user puts `<stack>` to the chest to `<input slot>`
messages = <messages>
}
```
The message contains an array of other messages (`<messages>`) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amount of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too.
The message contains an array of other messages (`<messages>`) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amount of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too. Only messages with the following actions can be included into a batch: "uput", "utake", "uswap", "umove", "empty", "full"; all the other messages are related to tube events. This restriction was added because, when we send a batch, the area might become unloaded, so we need to load it back, and the possibility to include messages of all types into a batch might make a possibility to create mechanisms which keep the area loaded by causing a large amount of events to a Digiline chest. Messages with actions "empty" and "full" are included into a batch only if the batch is already not empty. If the chest tries to send a message which cannot be batched while its current batch is not empty, the batch is sent immediately to preserve the original order of messages.

### Fields used within the messages

Expand Down
68 changes: 44 additions & 24 deletions inventory.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,45 @@ local S = digilines.S
local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil

-- Messages which will be sent in a single batch
local batched_messages = {}
local batches = {}
andriyndev marked this conversation as resolved.
Show resolved Hide resolved
-- Maximum interval from the previous message to include the current one into batch (in seconds)
local interval_to_batch = 0.1
-- Maximum number of messages in batch
local max_messages_in_batch = 100
-- Time of the last message for each chest
local last_message_time_for_chest = {}

-- Messages which can be included into batch
local can_be_batched = {
["empty"] = true, ["full"] = true, ["umove"] = true,
["uswap"] = true, ["utake"] = true, ["uput"] = true
}

-- Messages which shouldn't be included into batch when the batch is empty
local dont_batch_when_empty = { ["empty"] = true, ["full"] = true }

-- Sends the current batch message of a Digiline chest
-- pos: the position of the Digilines chest node
-- channel: the channel to which the message will be sent
local function send_and_clear_batch(pos, channel)
local pos_hash = minetest.hash_node_position(pos)
if #batched_messages[pos_hash] == 1 then
if #batches[pos_hash].messages == 1 then
-- If there is only one message is the batch, don't send it in a batch
digilines.receptor_send(pos, digilines.rules.default, channel,
batched_messages[pos_hash][1])
batches[pos_hash].messages[1])
else
digilines.receptor_send(pos, digilines.rules.default, channel, {
action = "batch",
messages = batched_messages[pos_hash]
messages = batches[pos_hash].messages
})
end
batched_messages[pos_hash] = nil
batches[pos_hash] = nil
last_message_time_for_chest[pos_hash] = nil
end

-- Send all the batched messages for the chest if present
local function send_batch_for_chest(pos)
if not batched_messages[minetest.hash_node_position(pos)] then
if not batches[minetest.hash_node_position(pos)] then
return
end
local channel = minetest.get_meta(pos):get_string("channel")
Expand Down Expand Up @@ -60,21 +69,31 @@ local function send_message(pos, action, stack, from_slot, to_slot, side)

-- Check if we need to include the current message into batch
local pos_hash = minetest.hash_node_position(pos)
local prev_time = last_message_time_for_chest[pos_hash] or 0
local cur_time = minetest.get_us_time()
last_message_time_for_chest[pos_hash] = cur_time
if cur_time - prev_time < 1000000 * interval_to_batch then
batched_messages[pos_hash] = batched_messages[pos_hash] or {}
table.insert(batched_messages[pos_hash], msg)
local node_timer = minetest.get_node_timer(pos)
if #batched_messages[pos_hash] >= max_messages_in_batch then
-- Send the batch immediately if it's full
node_timer:stop()
if can_be_batched[msg.action] and (batches[pos_hash] or not dont_batch_when_empty[msg.action]) then
local prev_time = last_message_time_for_chest[pos_hash] or 0
local cur_time = minetest.get_us_time()
last_message_time_for_chest[pos_hash] = cur_time
if cur_time - prev_time < 1000000 * interval_to_batch or batches[pos_hash] then
batches[pos_hash] = batches[pos_hash] or { messages = {} }
table.insert(batches[pos_hash].messages, msg)
if batches[pos_hash].timer then
batches[pos_hash].timer:cancel()
end
if #batches[pos_hash].messages >= max_messages_in_batch then
-- Send the batch immediately if it's full
send_and_clear_batch(pos, channel)
else
batches[pos_hash].timer = minetest.after(interval_to_batch, send_batch_for_chest, pos)
end

return
end
else
-- If the current message cannot be batched, flush the current batch to preserve order
if batches[pos_hash] then
batches[pos_hash].timer:cancel()
send_and_clear_batch(pos, channel)
else
node_timer:start(interval_to_batch)
end
return
end

digilines.receptor_send(pos, digilines.rules.default, channel, msg)
Expand Down Expand Up @@ -245,6 +264,12 @@ minetest.register_node("digilines:chest", {
inv:set_size("main", 8*4)
end,
on_destruct = function(pos)
local pos_hash = minetest.hash_node_position(pos)
if not batches[pos_hash] then
return
end

batches[pos_hash].timer:cancel()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

job:cancel() was added in 5.4.0. Increase the required version in README.md accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, got it. However I'm not sure what's the best: increase the supported version or make a workaround and not use job:cancel()

send_batch_for_chest(pos)
end,
after_place_node = tubescan,
Expand Down Expand Up @@ -382,11 +407,6 @@ minetest.register_node("digilines:chest", {
send_message(pos, "utake", stack, index)
check_empty(pos)
minetest.log("action", player:get_player_name().." takes stuff from chest at "..minetest.pos_to_string(pos))
end,
on_timer = function(pos, _)
-- Send all the batched messages when enough time since the last message passed
send_batch_for_chest(pos)
return false
end
})

Expand Down