Extension:CentralNotice/Allocation system

Banner allocation in CentralNotice is an iterative process across all active banners under a selector (project, language, country / mobile carrier, logged in/out, device, and bucket.) Under this selector banners have a priority (as assigned by the campaign they belong to) and two weighting factors (one given by the campaign, and one given by the banner inside the campaign.)

Given these data, the algorithm will attempt to fairly distribute the banners into 'slots'. A slot, of which there are a fixed number per selector, is a mechanism CentralNotice uses to manage the front-end cache. Slots are required because otherwise the end user has to make 2 calls up to the server, with the slot system the user simply chooses a single random number out of the known number. CentralNotice then returns the banner that was assigned that slot under that selector. At the moment CentralNotice has 30 slots per selector -- this number was chosen as a compromise between cache utilization and its number of factors.

Slots may hold multiple banners in a stack like configuration. This is possible because banners are allowed to choose not to display themselves, this is called an optional or non-terminal banner. A banner further down the stack will only be chosen if all banners above it in the slot have opted to not display themselves. A slot does not necessarily have to hold a banner. Further a slot which is 'non-terminal', e.g. contains nothing but non terminal banners, is not required to have a terminal banner at the end. These conditions allow CentralNotice to 'under allocate' slots if campaigns choose to not display 100% of the time.

(1) Requested Allocations
Determines the number of slots requested by a campaign if no other campaigns existed. This can be expressed as expectedAllocation = round( numberOfSlots * campaign.Weight )

(2) First pass, in priority allocation
Starting with the highest system priority, assigns slots, at least 1, to banners based on pure weights. This can be expressed as banner.Slots = max( floor[ numNonTerminalSlots * { campaign.Weight / max( sumOfCampaignWeightsInPriority, 100 ) } * { banner.Weight / 100 } ], 1 )

(3) Second pass, in priority, non terminal reallocation
Where there exist non terminal slots (they are either empty, or it contains only optional banners) distribute them to eligible campaigns. A campaign is eligible if its number of assigned slots is less than its expected allocation (it is 'under-allocated') and if there exist non terminal slots not already assigned to itself. This can be expressed as a while loop over the campaigns while (numNonTerminalSlots > 0) and underAllocatedCampaignsExist { foreach campaign { numAvailableSlots = max( floor[ numEligibleNonTerminalSlots(campaign) * campaignWeight / sumOfEligibleCampaignWeights(campaign) ], 1 ) totalNumAssignedSlots = 0 foreach banner in campaign { numAssignedSlots = max( floor[ numAvailableSlots * { banner.Weight / 100 } ], 1 ) banner.Slots += numAssignedSlots totalNumAssignedSlots += numAssignedSlots if ( numAssignedSlots == numAvailableSlots ) { break }       }    } }

(4) Lower priority allocation
If there exist non terminal slots, and there exist campaigns in priority levels lower than this one, repeat 2 & 3 at the next lowest priority level with a campaign.

Examples
To demonstrate how the algorithm actually works, imagine we have a system with 6 slots.

Under allocated, single priority
Under a given selector we have 1 campaign as shown:

1) Requested Allocations

2) First pass, in priority allocation

Therefore our slot allocation looks like this

3) Second pass, in priority, non terminal reallocation

No under-allocated campaigns exist in the current priority; therefore no change.

4) Lower priority allocation

No lower priority campaigns exist; therefore no change.

5) Final slot allocation

Note: The algorithm attempts to preserve the requested weights, but there is quantization error. In this case we ended up with Aa = 33% and Ab = 16.6%.

Under allocated, multiple priority
Under a given selector we have 1 campaign as shown:

1) Requested Allocations

2) First pass, high priority allocation

Therefore our slot allocation looks like this

3) Second pass, high priority, non terminal reallocation

No under-allocated campaigns exist in the current priority; therefore no change.

4) First pass, low priority allocation

Therefore our slot allocation looks like this

5) Second pass, low priority, non terminal reallocation

No under-allocated campaigns exist in the current priority; therefore no change.

6) Final slot allocation

Fully allocated, multiple priority, with optional banners
Under a given selector we have 4 campaigns (at two priority levels, and various weights) as shown: * Indicates an optional banner

1) Requested Allocations

2) First pass, high priority allocation

Therefore our slot allocation looks like this

3) Second pass, high priority, non terminal reallocation
 * Campaign A is under allocated 1 slot, and there are 2 non terminal eligible slots. Therefore, A is given 1 additional slot which it allocates to Aa.
 * Campaign B is over allocated and gets no slots
 * Campaign C is under allocated 1 slot, and there are 3 non terminal eligible slots. Therefore C is given 1 additional slot which it allocates to Ca

Therefore our slot allocation looks like this

4) First pass, low priority allocation There are now two non terminal slots to allocate to the low priority.

Therefore our slot allocation looks like this

5) Second pass, low priority, non terminal reallocation No under-allocated campaigns exist in the current priority; therefore no change.

6) Final slot allocation