This how-to guide assumes you already completed the steps described in the 5-minute getting started experience. To test that the Nextmv CLI is correctly configured, you can optionally run the following command on your terminal. It will get some files that are necessary to work with the Nextmv Platform. You can see the expected output as well.
The Nextmv Software Development Kit (SDK) lets you automate any operational decision in a way that looks and feels like writing other code. It provides the guardrails to turn your data into automated decisions and test and deploy them into production environments.
Introduction
This guide will walk you through our pager-duty
template. To get the template, simply run the following command.
You can check that all files are available in the newly created pager-duty
folder. You can check the structure of the folder.
LICENSE
contains the Apache License 2.0 under which we distribute this template.README.md
gives a short introduction to the PagerDuty problem and shows you how to run the template.go.mod
andgo.sum
define a Go module and are used to manage dependencies, including the Nextmv SDK.input.json
describes the input data for a specific PagerDuty problem that is solved by the template.main.go
contains the actual code ofpager-duty
app.- The
pager-duty.code-workspace
file should be used to open the template in Visual Studio Code. It is pre-configured for you so that you can run and debug the template without any further steps.
Now you can run the template with the Nextmv CLI, reading from the input.json
file and writing to an output.json
file. The following command shows you how to specify solver limits as well. You should obtain an output similar to the one shown.
Note that transient fields like timestamps, duration, versions, etc. are represented with dummy values due to their dynamic nature. I.e., every time the input is run or the version is bumped, these fields will have a different value.
Now we will show you, step by step, what the code inside the main.go
achieves.
Dissecting the app
The first part of the main.go
defines a package name, imports packages that are needed by the model and creates a main
function - the starting point for the app. We create a runner using the Run
function from the Nextmv run
package. This function executes a solver
, defined below.
The Input
But before we look into the solver
function, we will examine the structs: input
, user
, and preference
.
The input
struct has three fields. ScheduleStart
and ScheduleEnd
define the full scheduling window to plan for and Users
, which holds a slice of users who need to be scheduled. The User
struct has a Name
(to identify each individual), Id
and Type
(PagerDuty specific fields), and Unavailable
days and preference
, an array of user Preferences
, both in RFC3339 format for ease of passing into PagerDuty.
The Output
Now that we've defined the input that will be run through our model, we need to define the output. Since this data will be pushed through to PagerDuty, we created two output structs for ease of sending our solution directly to their API: override
and assignedUser
.
override
provides the Start
, End
, User
, and Timezone
of the override to work with PagerDuty. And assignedUser
defines the Name
, Id
, and Type
of the user assigned to that particular day.
Note that the json:"XXX"
after the fields make use of a Go feature to automatically map the data from the input.json
to those entities and fields.
The Solver
The solver
function is where the model is defined. The function's signature adheres to the run.Run
function we saw earlier already.
When you first ran the template you passed in the parameter -runner.input.path
followed by the path to an input file. This file is automatically parsed and converted to our input
struct. Other option arguments are also interpreted automatically and passed to the solver as an Options
struct.
Our end goal is to assign a single user for each day in the schedule period. To accomplish this, within the solver function, we start by initializing an empty root store.
Next, add a starting state to store. Create a domain for each day and initialize each day to all users. In other words, the root state begins by assuming every user is available on every day and we'll trim down from there.
Next, we initialize some variables for use later in the Value function. assignedDays
will be used to ensure a fair and balanced schedule across the team while happiness
tracks how often we fulfill user preferences
.
Now, we make some modifications to our root schedule. We loop through each day and then user. If the user is unavailable on that day, we remove that user’s index from the domain of available users on that day using the days.Remove(..) logic below.
Note that any operations on store variables such as .Remove
will return a slice of changes. We must then apply those changes to the root store in order to update it.
We populate a preferenceMap
which creates a lookup table of date index to a slice of user indices for the users who prefer to be on-call that day.
While looping through days, we check whether there's any day where no one is available to work. If so, this is considered infeasible, and the run will exit with an error.
Lastly, we check whether there are any days with exactly 1 user available. If so, assign that user and increment their assignedDays
by 1, and increase happiness
by 1 if this was a preferred day.
The Generate function
The Generate
function is used to generate the search tree and traverse the search space. In this template, the Generate
function finds the first day with 2 or more users available. It then loops through each available user for that day and creates a child store in which we assign that user to the day and increase assignedDays
and happiness
, where applicable.
This returns an eager generater, meaning all the child stores are created upfront.
The Validate function
The Validate
function checks whether a store is an operationally valid solution or not. For our PagerDuty schedule, this means that there’s exactly one user assigned to each day.
The Value function
Now we calculate the store's value
to evaluate whether one store is better than another. As mentioned before, we want to create a fair schedule by balancing days assigned across users and maximizing happiness.
In order to balance days assigned, we compute the minimum and maximum of the assignedDays
slice. We want the difference between those two values to be small, meaning there’s a small range in the number of days assigned to each person.
In order to maximize happiness, we focus on maximizing the minimum happiness score. This inherently attempts to bump up the happiness for each user.
The Format function
Using the Format
function we change the output format to make it easier to read.
The function itself is defined like this:
In this case, we want to easily paste the output into a POST request to PagerDuty. We want to output an array of override
. To do this, we loop through each day in our schedule and create an override for each day. (We also added on the min assigned days to our output for review as well.)
Returning the solver
Finally, we return a solver
for our store input
by using the Minimizer
function passing in options that were given at the very beginning by the calling function. This solver is then executed by the run.Run
function from the beginning.