How to customize how workers are ranked for the best worker distribution mode
The best-worker
distribution mode selects the workers that are best able to handle the job first. The logic to rank Workers can be customized, with an expression or Azure function to compare two workers. The following example shows how to customize this logic with your own Azure Function.
Scenario: Custom scoring rule in best worker distribution mode
We want to distribute offers among their workers associated with a queue. The workers will be given a score based on their labels and skill set. The worker with the highest score should get the first offer (BestWorker Distribution Mode).
Situation
- A job has been created and classified.
- Job has the following labels associated with it
- ["CommunicationType"] = "Chat"
- ["IssueType"] = "XboxSupport"
- ["Language"] = "en"
- ["HighPriority"] = true
- ["SubIssueType"] = "ConsoleMalfunction"
- ["ConsoleType"] = "XBOX_SERIES_X"
- ["Model"] = "XBOX_SERIES_X_1TB"
- Job has the following WorkerSelectors associated with it
- ["English"] >= 7
- ["ChatSupport"] = true
- ["XboxSupport"] = true
- Job has the following labels associated with it
- Job currently is in a state of 'Queued'; enqueued in Xbox Hardware Support Queue waiting to be matched to a worker.
- Multiple workers become available simultaneously.
- Worker 1 has been created with the following labels
- ["HighPrioritySupport"] = true
- ["HardwareSupport"] = true
- ["Support_XBOX_SERIES_X"] = true
- ["English"] = 10
- ["ChatSupport"] = true
- ["XboxSupport"] = true
- Worker 2 has been created with the following labels
- ["HighPrioritySupport"] = true
- ["HardwareSupport"] = true
- ["Support_XBOX_SERIES_X"] = true
- ["Support_XBOX_SERIES_S"] = true
- ["English"] = 8
- ["ChatSupport"] = true
- ["XboxSupport"] = true
- Worker 3 has been created with the following labels
- ["HighPrioritySupport"] = false
- ["HardwareSupport"] = true
- ["Support_XBOX"] = true
- ["English"] = 7
- ["ChatSupport"] = true
- ["XboxSupport"] = true
- Worker 1 has been created with the following labels
Expectation
We would like the following behavior when scoring workers to select which worker gets the first offer.
The decision flow (as shown above) is as follows:
If a job is NOT HighPriority:
- Workers with label: ["Support_XBOX"] = true; get a score of 100
- Otherwise, get a score of 1
If a job is HighPriority:
- Workers with label: ["HighPrioritySupport"] = false; get a score of 1
- Otherwise, if ["HighPrioritySupport"] = true:
- Does Worker specialize in console type -> Does worker have label: ["Support_<jobLabels.ConsoleType>"] = true? If true, worker gets score of 200
- Otherwise, get a score of 100
Creating an Azure function
Before moving on any further in the process, let us first define an Azure function that scores worker.
Note
The following Azure function is using JavaScript. For more information, please refer to Quickstart: Create a JavaScript function in Azure using Visual Studio Code
Sample input for Worker 1
{
"job": {
"CommunicationType": "Chat",
"IssueType": "XboxSupport",
"Language": "en",
"HighPriority": true,
"SubIssueType": "ConsoleMalfunction",
"ConsoleType": "XBOX_SERIES_X",
"Model": "XBOX_SERIES_X_1TB"
},
"selectors": [
{
"key": "English",
"operator": "GreaterThanEqual",
"value": 7,
"expiresAfterSeconds": null
},
{
"key": "ChatSupport",
"operator": "Equal",
"value": true,
"expiresAfterSeconds": null
},
{
"key": "XboxSupport",
"operator": "Equal",
"value": true,
"expiresAfterSeconds": null
}
],
"worker": {
"Id": "e3a3f2f9-3582-4bfe-9c5a-aa57831a0f88",
"HighPrioritySupport": true,
"HardwareSupport": true,
"Support_XBOX_SERIES_X": true,
"English": 10,
"ChatSupport": true,
"XboxSupport": true
}
}
Sample implementation:
module.exports = async function (context, req) {
context.log('Best Worker Distribution Mode using Azure Function');
let score = 0;
const jobLabels = req.body.job;
const workerLabels = req.body.worker;
const isHighPriority = !!jobLabels["HighPriority"];
context.log('Job is high priority? Status: ' + isHighPriority);
if(!isHighPriority) {
const isGenericXboxSupportWorker = !!workerLabels["Support_XBOX"];
context.log('Worker provides general xbox support? Status: ' + isGenericXboxSupportWorker);
score = isGenericXboxSupportWorker ? 100 : 1;
} else {
const workerSupportsHighPriorityJob = !!workerLabels["HighPrioritySupport"];
context.log('Worker provides high priority support? Status: ' + workerSupportsHighPriorityJob);
if(!workerSupportsHighPriorityJob) {
score = 1;
} else {
const key = `Support_${jobLabels["ConsoleType"]}`;
const workerSpecializeInConsoleType = !!workerLabels[key];
context.log(`Worker specializes in consoleType: ${jobLabels["ConsoleType"]} ? Status: ${workerSpecializeInConsoleType}`);
score = workerSpecializeInConsoleType ? 200 : 100;
}
}
context.log('Final score of worker: ' + score);
context.res = {
// status: 200, /* Defaults to 200 */
body: score
};
}
Output for Worker 1
200
With the aforementioned implementation, for the given job we'll get the following scores for workers:
Worker | Score |
---|---|
Worker 1 | 200 |
Worker 2 | 200 |
Worker 3 | 1 |
Distribute offers based on best worker mode
Now that the Azure function app is ready, let us create an instance of BestWorkerDistribution mode using Router SDK.
var administrationClient = new JobRouterAdministrationClient("<YOUR_ACS_CONNECTION_STRING>");
// Setup Distribution Policy
var distributionPolicy = await administrationClient.CreateDistributionPolicyAsync(
new CreateDistributionPolicyOptions(
distributionPolicyId: "BestWorkerDistributionMode",
offerExpiresAfter: TimeSpan.FromMinutes(5),
mode: new BestWorkerMode(scoringRule: new FunctionRouterRule(new Uri("<insert function url>")))
) { Name = "XBox hardware support distribution" });
// Setup Queue
var queue = await administrationClient.CreateQueueAsync(
new CreateQueueOptions(
queueId: "XBox_Hardware_Support_Q",
distributionPolicyId: distributionPolicy.Value.Id
) { Name = "XBox Hardware Support Queue" });
// Create workers
var worker1 = await client.CreateWorkerAsync(new CreateWorkerOptions(workerId: "Worker_1", capacity: 100)
{
Queues = { queue.Value.Id },
Channels = { new RouterChannel(channelId: "Xbox_Chat_Channel", capacityCostPerJob: 10) },
Labels =
{
["English"] = new RouterValue(10),
["HighPrioritySupport"] = new RouterValue(true),
["HardwareSupport"] = new RouterValue(true),
["Support_XBOX_SERIES_X"] = new RouterValue(true),
["ChatSupport"] = new RouterValue(true),
["XboxSupport"] = new RouterValue(true)
}
});
var worker2 = await client.CreateWorkerAsync(new CreateWorkerOptions(workerId: "Worker_2", capacity: 100)
{
Queues = { queue.Value.Id },
Channels = { new RouterChannel(channelId: "Xbox_Chat_Channel", capacityCostPerJob: 10) },
Labels =
{
["English"] = new RouterValue(8),
["HighPrioritySupport"] = new RouterValue(true),
["HardwareSupport"] = new RouterValue(true),
["Support_XBOX_SERIES_X"] = new RouterValue(true),
["ChatSupport"] = new RouterValue(true),
["XboxSupport"] = new RouterValue(true)
}
});
var worker3 = await client.CreateWorkerAsync(new CreateWorkerOptions(workerId: "Worker_3", capacity: 100)
{
Queues = { queue.Value.Id },
Channels = { new RouterChannel(channelId: "Xbox_Chat_Channel", capacityCostPerJob: 10) },
Labels =
{
["English"] = new RouterValue(7),
["HighPrioritySupport"] = new RouterValue(true),
["HardwareSupport"] = new RouterValue(true),
["Support_XBOX_SERIES_X"] = new RouterValue(true),
["ChatSupport"] = new RouterValue(true),
["XboxSupport"] = new RouterValue(true)
}
});
// Create Job
var job = await client.CreateJobAsync(
new CreateJobOptions(jobId: "job1", channelId: "Xbox_Chat_Channel", queueId: queue.Value.Id)
{
Priority = 100,
ChannelReference = "ChatChannel",
RequestedWorkerSelectors =
{
new RouterWorkerSelector(key: "English", labelOperator: LabelOperator.GreaterThanEqual, value: new RouterValue(7)),
new RouterWorkerSelector(key: "ChatSupport", labelOperator: LabelOperator.Equal, value: new RouterValue(true)),
new RouterWorkerSelector(key: "XboxSupport", labelOperator: LabelOperator.Equal, value: new RouterValue(true))
},
Labels =
{
["CommunicationType"] = new RouterValue("Chat"),
["IssueType"] = new RouterValue("XboxSupport"),
["Language"] = new RouterValue("en"),
["HighPriority"] = new RouterValue(true),
["SubIssueType"] = new RouterValue("ConsoleMalfunction"),
["ConsoleType"] = new RouterValue("XBOX_SERIES_X"),
["Model"] = new RouterValue("XBOX_SERIES_X_1TB")
}
});
// Wait a few seconds and see which worker was matched
await Task.Delay(TimeSpan.FromSeconds(5));
var getJob = await client.GetJobAsync(job.Value.Id);
Console.WriteLine(getJob.Value.Assignments.Select(assignment => assignment.Value.WorkerId).First());
Output
Worker_1 // or Worker_2
Since both workers, Worker_1 and Worker_2, get the same score of 200,
the worker who has been idle the longest will get the first offer.