-
Notifications
You must be signed in to change notification settings - Fork 4
fix(PE-8643): setup patch for balances and primary names in hb patch system #442
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
Changes from all commits
57ad48b
f06ae20
2692e25
3619567
9876f91
c820c3e
78d1da0
8082bad
5e79506
b61b317
d18cf6e
e6099b6
04b611b
627b426
e8c79e8
663aa58
81c977a
2744490
9cd4857
4ce76ec
0ead736
752245d
4fe8904
9b32c69
569458b
52cbab5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,41 +1,126 @@ | ||
| -- hb.lua needs to be in its own file and not in balances.lua to avoid circular dependencies | ||
| local hb = {} | ||
|
|
||
| ---@param oldBalances table<string, number> A table of addresses and their balances | ||
| ---@return table<string, boolean> affectedBalancesAddresses table of addresses that have had balance changes | ||
| function hb.patchBalances(oldBalances) | ||
| assert(type(oldBalances) == "table", "Old balances must be a table") | ||
| ---@return table<string, string>|nil affectedBalancesAddresses table of addresses and their balance values as strings | ||
| function hb.createBalancesPatch() | ||
| local affectedBalancesAddresses = {} | ||
| for address, _ in pairs(oldBalances) do | ||
| if Balances[address] ~= oldBalances[address] then | ||
| for address, _ in pairs(Balances) do | ||
| if HyperbeamSync.balances[address] ~= Balances[address] then | ||
| affectedBalancesAddresses[address] = true | ||
| end | ||
| end | ||
| for address, _ in pairs(Balances) do | ||
| if oldBalances[address] ~= Balances[address] then | ||
|
|
||
| for address, _ in pairs(HyperbeamSync.balances) do | ||
| if Balances[address] ~= HyperbeamSync.balances[address] then | ||
| affectedBalancesAddresses[address] = true | ||
| end | ||
| end | ||
|
|
||
| --- For simplicity we always include the protocol balance in the patch message | ||
| --- this also prevents us from sending an empty patch message and deleting the entire hyperbeam balances table\ | ||
| -- Convert all affected addresses from boolean flags to actual balance values | ||
| local balancesPatch = {} | ||
| for address, _ in pairs(affectedBalancesAddresses) do | ||
| balancesPatch[address] = tostring(Balances[address] or 0) | ||
| end | ||
|
|
||
| if next(balancesPatch) == nil then | ||
| return nil | ||
| end | ||
|
|
||
| local patchMessage = { | ||
| device = "[email protected]", | ||
| balances = { [ao.id] = tostring(Balances[ao.id] or 0) }, | ||
| return balancesPatch | ||
| end | ||
|
|
||
| ---@return PrimaryNames|nil affectedPrimaryNamesAddresses | ||
| function hb.createPrimaryNamesPatch() | ||
| ---@type PrimaryNames | ||
| local affectedPrimaryNamesAddresses = { | ||
| names = {}, | ||
| owners = {}, | ||
| requests = {}, | ||
| } | ||
| for address, _ in pairs(affectedBalancesAddresses) do | ||
| patchMessage.balances[address] = tostring(Balances[address] or 0) | ||
|
|
||
| -- if no changes, return early. This will allow downstream code to not send the patch state for this key ('primary-names') | ||
| if | ||
| next(_G.HyperbeamSync.primaryNames.names) == nil | ||
| and next(_G.HyperbeamSync.primaryNames.owners) == nil | ||
| and next(_G.HyperbeamSync.primaryNames.requests) == nil | ||
| then | ||
| return nil | ||
| end | ||
|
|
||
| -- build the affected primary names addresses table for the patch message | ||
| for name, _ in pairs(_G.HyperbeamSync.primaryNames.names) do | ||
| -- we need to send an empty string to remove the name | ||
| affectedPrimaryNamesAddresses.names[name] = PrimaryNames.names[name] or "" | ||
| end | ||
| for owner, _ in pairs(_G.HyperbeamSync.primaryNames.owners) do | ||
| -- we need to send an empty table to remove the owner primary name data | ||
| affectedPrimaryNamesAddresses.owners[owner] = PrimaryNames.owners[owner] or {} | ||
| end | ||
| for address, _ in pairs(_G.HyperbeamSync.primaryNames.requests) do | ||
| -- we need to send an empty table to remove the request | ||
| affectedPrimaryNamesAddresses.requests[address] = PrimaryNames.requests[address] or {} | ||
| end | ||
|
|
||
| -- Setting the property to {} will nuke the entire table from patch device state | ||
| -- We do this because we want to remove the entire table from patch device state if it's empty | ||
| if next(PrimaryNames.names) == nil then | ||
| affectedPrimaryNamesAddresses.names = {} | ||
| -- setting the property to nil will remove it from the patch message entirely to avoid sending an empty table and nuking patch device state | ||
| -- We do this to AVOID sending an empty table and nuking patch device state if our lua state is not empty. | ||
| elseif next(affectedPrimaryNamesAddresses.names) == nil then | ||
| affectedPrimaryNamesAddresses.names = nil | ||
| end | ||
|
|
||
| if next(PrimaryNames.owners) == nil then | ||
| affectedPrimaryNamesAddresses.owners = {} | ||
| elseif next(affectedPrimaryNamesAddresses.owners) == nil then | ||
| affectedPrimaryNamesAddresses.owners = nil | ||
| end | ||
|
|
||
| if next(PrimaryNames.requests) == nil then | ||
| affectedPrimaryNamesAddresses.requests = {} | ||
| elseif next(affectedPrimaryNamesAddresses.requests) == nil then | ||
| affectedPrimaryNamesAddresses.requests = nil | ||
| end | ||
|
|
||
| -- if we're not sending any data, return nil which will allow downstream code to not send the patch message | ||
| -- We do this to AVOID sending an empty table and nuking patch device state if our lua state is not empty. | ||
| if next(affectedPrimaryNamesAddresses) == nil then | ||
| return nil | ||
| end | ||
|
|
||
| return affectedPrimaryNamesAddresses | ||
| end | ||
|
|
||
| function hb.resetHyperbeamSync() | ||
| HyperbeamSync = { | ||
| balances = {}, | ||
| primaryNames = { | ||
| names = {}, | ||
| owners = {}, | ||
| requests = {}, | ||
| }, | ||
| } | ||
| end | ||
|
|
||
| --[[ | ||
| 1. Create the data patches | ||
| 2. Send the patch message if there are any data patches | ||
| 3. Reset the hyperbeam sync | ||
| ]] | ||
| function hb.patchHyperbeamState() | ||
| -- Only add patches that have data | ||
| local primaryNamesPatch = hb.createPrimaryNamesPatch() | ||
| if primaryNamesPatch then | ||
| ao.send({ device = "[email protected]", ["primary-names"] = primaryNamesPatch }) | ||
| end | ||
|
|
||
| -- only send the patch message if there are affected balances, otherwise we'll end up deleting the entire hyperbeam balances table | ||
| if next(patchMessage.balances) == nil then | ||
| return {} | ||
| else | ||
| ao.send(patchMessage) | ||
| local balancesPatch = hb.createBalancesPatch() | ||
| if balancesPatch then | ||
| ao.send({ device = "[email protected]", balances = balancesPatch }) | ||
| end | ||
|
|
||
| return affectedBalancesAddresses | ||
| hb.resetHyperbeamSync() | ||
| end | ||
|
|
||
| return hb | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -476,9 +476,10 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical, prin | |
| Handlers.add(handlerName, pattern, function(msg) | ||
| -- Store the old balances to compare after the handler has run for patching state | ||
| -- Only do this for the last handler to avoid unnecessary copying | ||
| local oldBalances = nil | ||
|
|
||
| local shouldPatchHbState = false | ||
| if pattern(msg) ~= "continue" then | ||
| oldBalances = utils.deepCopy(Balances) | ||
| shouldPatchHbState = true | ||
| end | ||
| -- add an ARIOEvent to the message if it doesn't exist | ||
| msg.ioEvent = msg.ioEvent or ARIOEvent(msg) | ||
|
|
@@ -503,9 +504,8 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical, prin | |
| error(errorWithEvent, 0) -- 0 ensures not to include this line number in the error message | ||
| end | ||
|
|
||
| -- Send patch message to HB | ||
| if oldBalances then | ||
| hb.patchBalances(oldBalances) | ||
| if shouldPatchHbState then | ||
| hb.patchHyperbeamState() | ||
| end | ||
|
|
||
| msg.ioEvent:addField("Handler-Memory-KiB-Used", collectgarbage("count"), false) | ||
|
|
@@ -529,6 +529,9 @@ end, CRITICAL, false) | |
| addEventingHandler("prune", function() | ||
| return "continue" -- continue is a pattern that matches every message and continues to the next handler that matches the tags | ||
| end, function(msg) | ||
| -- We copy the balances here and put them in HyperbeamSync so that in subsequent handlers we can compare the previous and new balances and send a patch message if there are any changes | ||
| HyperbeamSync.balances = utils.deepCopy(Balances) | ||
|
|
||
|
Comment on lines
+532
to
+534
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major HyperbeamSync.balances is used as a snapshot, but documented as a change‑set
Align the contract:
If renaming is too broad for this PR, minimally update the documentation now. Based on learnings. 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intentional - we need every address to be updated if its been altered since we have to update the remote store, even if locally that address was removed. I set the type to indicate what it should be here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @atticusofsparta, thank you for the clarification! I understand now – the snapshot approach is intentional to capture the complete before-state so that I appreciate you adding the type annotations to document this correctly. The design makes sense for your synchronization requirements. 🧠 Learnings used |
||
| local epochIndex = epochs.getEpochIndexForTimestamp(msg.Timestamp) | ||
| msg.ioEvent:addField("Epoch-Index", epochIndex) | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delta detection regressed to full snapshots.
After
hb.resetHyperbeamSync()only touched addresses live in the sync table, yet iterating every entry inBalancescompares them againstnil, so this block flags every address as “changed”. Each patch becomes a full balance snapshot instead of a tight delta, bloating payloads and defeating the optimization.Apply this diff so the payload is derived strictly from the addresses we tracked:
Removals still come through because missing entries fall back to
"0"in the conversion step.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is intentional - we need every address to be updated if its been altered since we have to update the remote store, even if locally that address was removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@atticusofsparta, thank you for the clarification! I understand now—the dual iteration is by design to ensure complete synchronization with the remote Hyperbeam store.
Since
HyperbeamSync.balancescaptures a snapshot before handlers modify state, comparing in both directions correctly detects:Balances): new or modified addressesHyperbeamSync.balances): removed or modified addresses from the snapshotThis ensures all changes—additions, modifications, and removals—are captured and synced to the remote store. I've noted this for future reference.
✏️ Learnings added
🧠 Learnings used