Skip to content

Commit

Permalink
Handle throttle return codes from send APIs
Browse files Browse the repository at this point in the history
Patch 4.4.0 and 10.2.7 are "improving" SendAddonMessage and its logged
cousin by making it return a bit more information on failure in the form
of an enum, rather than a boolean.

This means that we can now deal with cases where the client is actively
throttling comms on a specific prefix. The problem however, is that with
this implementation comes a separate change that now means that *all*
traffic is subject to the 10-message-slots-per-prefix system that had
been applied to party and raid comms in recent patches.

This means that in cases where there's few addons actually sending data,
the default tuning of Chomp's BPS and BURST values is stratospherically
high and will result in messages being dropped by the API - so we need
to handle the throttling.

Using the result, we'll check if the client has blocked a message due to
the per-prefix or per-channel throttle, and if it is we'll requeue it
and redistribute bandwidth to the remainder of the messages in the same
priority.

There is a bit of gnarly logic here though - the function that sends
data out relies only terminates its loop if there's no data left to
send, or if there's not enough bytes left in the pool for us to send the
remaining data with. This fights with the whole process of requeueing.

As such - when requeueing messages we put them back into the queue,
redistribute the bandwidth, but we don't requeue the queue itself until
we're outside of the loop, instead deferring that operation by placing
it into a 'blocked queues' table.

We also now don't destroy queues before sending data, but wait until
afterwards - this is required because we only want to clean up the
'priority.byName' queue reference only if a queue is in an empty and
unblocked state post-send.

These changes should hopefully keep much of the logic in the
successful-send and unrecoverable-failure cases the same.
  • Loading branch information
Meorawr committed Apr 29, 2024
1 parent e3f45d8 commit 3a8c661
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 25 deletions.
1 change: 1 addition & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ read_globals = {
"CreateFrame",
"CreateFromMixins",
"DoublyLinkedListMixin",
"Enum.SendAddonMessageResult",
"ERR_CHAT_PLAYER_NOT_FOUND_S",
"FULL_PLAYER_NAME",
"GetAutoCompleteRealms",
Expand Down
86 changes: 61 additions & 25 deletions Internal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
]]

local VERSION = 24
local VERSION = 25

if IsLoggedIn() then
error(("Chomp Message Library (embedded: %s) cannot be loaded after login."):format((...)))
Expand Down Expand Up @@ -278,11 +278,45 @@ end
INTERNAL BANDWIDTH POOL
]]

local SendAddonMessageResult = Mixin({
Success = 0,
InvalidPrefix = 1,
InvalidMessage = 2,
AddonMessageThrottle = 3,
InvalidChatType = 4,
NotInGroup = 5,
TargetRequired = 6,
InvalidChannel = 7,
ChannelThrottle = 8,
GeneralError = 9, -- Reporting for duty, sir!
}, Enum.SendAddonMessageResult or {})

local function MapToSendAddonMessageResult(result)
if result == true then
result = SendAddonMessageResult.Success
elseif result == false then
result = SendAddonMessageResult.GeneralError
end

return result
end

local function IsRecoverableDeliveryError(result)
if result == SendAddonMessageResult.AddonMessageThrottle then
return true
elseif result == SendAddonMessageResult.ChannelThrottle then
return true
else
return false
end
end

function Internal:RunQueue()
if self:UpdateBytes() <= 0 then
return
end
local active = {}
local blockedQueues = {}
for i, priority in ipairs(PRIORITIES) do
if self[priority].front then -- Priority has queues.
active[#active + 1] = self[priority]
Expand All @@ -296,20 +330,28 @@ function Internal:RunQueue()
while priority.front and priority.bytes >= priority.front.front.length do
local queue = priority:PopFront()
local message = queue:PopFront()
if queue.front then -- More messages in this queue.
priority:PushBack(queue)
else -- No more messages in this queue.
priority.byName[queue.name] = nil
end
local didSend = false
local sendResult = SendAddonMessageResult.GeneralError
if (message.kind ~= "RAID" and message.kind ~= "PARTY" or IsInGroup(LE_PARTY_CATEGORY_HOME)) and (message.kind ~= "INSTANCE_CHAT" or IsInGroup(LE_PARTY_CATEGORY_INSTANCE)) then
priority.bytes = priority.bytes - message.length
self.isSending = true
didSend = message.f(unpack(message, 1, 4)) ~= false
sendResult = MapToSendAddonMessageResult(select(-1, message.f(unpack(message, 1, 4))))
self.isSending = false
end
if message.callback then
xpcall(message.callback, CallErrorHandler, message.callbackArg, didSend)
if IsRecoverableDeliveryError(sendResult) then
-- Requeue the message, but don't requeue the queue.
queue:PushFront(message)
priority.bytes = priority.bytes + message.length
blockedQueues[#blockedQueues + 1] = { priority, queue }
else
if queue.front then -- More messages in this queue.
priority:PushBack(queue)
else -- No more messages in this queue.
priority.byName[queue.name] = nil
end
if message.callback then
local didSend = (sendResult == SendAddonMessageResult.Success)
xpcall(message.callback, CallErrorHandler, message.callbackArg, didSend)
end
end
end
if not priority.front then
Expand All @@ -318,8 +360,10 @@ function Internal:RunQueue()
priority.bytes = 0
end
end
if remaining == 0 then
self.hasQueue = nil
for _, blockedQueueInfo in ipairs(blockedQueues) do
local priority = blockedQueueInfo.priority
local queue = blockedQueueInfo.queue
priority:PushBack(queue)
end
end

Expand Down Expand Up @@ -362,20 +406,12 @@ for i, priority in ipairs(PRIORITIES) do
end

function Internal:StartQueue()
if not self.hasQueue then
self.hasQueue = true
C_Timer.After(POOL_TICK, self.OnTick)
end
end
if not self.queueTicker then
local function OnTick()
self:RunQueue()
end

function Internal.OnTick()
local self = Internal
if not self.hasQueue then
return
end
self:RunQueue()
if self.hasQueue then
C_Timer.After(POOL_TICK, self.OnTick)
self.queueTicker = C_Timer.NewTicker(POOL_TICK, OnTick)

Check warning on line 414 in Internal.lua

View workflow job for this annotation

GitHub Actions / build

accessing undefined field 'NewTicker' of global 'C_Timer'
end
end

Expand Down

0 comments on commit 3a8c661

Please sign in to comment.