forked from target/goalert
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstore.go
289 lines (262 loc) · 7.68 KB
/
store.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
package oncall
import (
"context"
"database/sql"
"time"
"github.com/target/goalert/assignment"
"github.com/target/goalert/override"
"github.com/target/goalert/permission"
"github.com/target/goalert/schedule/rule"
"github.com/target/goalert/util"
"github.com/target/goalert/util/sqlutil"
"github.com/target/goalert/validation/validate"
"github.com/pkg/errors"
)
// Store allows retrieving and calculating on-call information.
type Store interface {
OnCallUsersByService(ctx context.Context, serviceID string) ([]ServiceOnCallUser, error)
HistoryBySchedule(ctx context.Context, scheduleID string, start, end time.Time) ([]Shift, error)
}
// ServiceOnCallUser represents a currently on-call user for a service.
type ServiceOnCallUser struct {
StepNumber int `json:"step_number"`
UserID string `json:"user_id"`
UserName string `json:"user_name"`
}
// A Shift represents a duration a user is on-call.
// If truncated is true, then the End time does not represent
// the time the user stopped being on call, instead it indicates
// they were still on-call at that time.
type Shift struct {
UserID string `json:"user_id"`
Start time.Time `json:"start_time"`
End time.Time `json:"end_time"`
Truncated bool `json:"truncated"`
}
// DB implements the Store interface from Postgres.
type DB struct {
db *sql.DB
onCallUsersSvc *sql.Stmt
schedOverrides *sql.Stmt
schedOnCall *sql.Stmt
schedTZ *sql.Stmt
schedRot *sql.Stmt
rotParts *sql.Stmt
ruleStore rule.Store
}
// NewDB will create a new DB, preparing required statements using the provided context.
func NewDB(ctx context.Context, db *sql.DB, ruleStore rule.Store) (*DB, error) {
p := &util.Prepare{DB: db, Ctx: ctx}
return &DB{
db: db,
ruleStore: ruleStore,
schedOverrides: p.P(`
select
start_time,
end_time,
add_user_id,
remove_user_id
from user_overrides
where
tgt_schedule_id = $1 and
end_time > now() and
($2, $3) OVERLAPS(start_time, end_time)
`),
onCallUsersSvc: p.P(`
select step.step_number, oc.user_id, u.name as user_name
from services svc
join escalation_policy_steps step on step.escalation_policy_id = svc.escalation_policy_id
join ep_step_on_call_users oc on oc.ep_step_id = step.id and oc.end_time isnull
join users u on oc.user_id = u.id
where svc.id = $1
order by step.step_number, oc.start_time
`),
schedOnCall: p.P(`
select
user_id,
start_time,
end_time
from schedule_on_call_users
where
schedule_id = $1 and
($2, $3) OVERLAPS (start_time, coalesce(end_time, 'infinity')) and
(end_time isnull or (end_time - start_time) > '1 minute'::interval)
`),
schedTZ: p.P(`select time_zone, now() from schedules where id = $1`),
schedRot: p.P(`
select distinct
rot.id,
rot.type,
rot.start_time,
rot.shift_length,
rot.time_zone,
state.position,
state.shift_start
from schedule_rules rule
join rotations rot on rot.id = rule.tgt_rotation_id
join rotation_state state on state.rotation_id = rule.tgt_rotation_id
where rule.schedule_id = $1 and rule.tgt_rotation_id notnull
`),
rotParts: p.P(`
select
rotation_id,
user_id
from rotation_participants
where rotation_id = any($1)
order by
rotation_id,
position
`),
}, p.Err
}
// OnCallUsersByService will return the current set of users who are on-call for the given service.
func (db *DB) OnCallUsersByService(ctx context.Context, serviceID string) ([]ServiceOnCallUser, error) {
err := permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return nil, err
}
err = validate.UUID("ServiceID", serviceID)
if err != nil {
return nil, err
}
rows, err := db.onCallUsersSvc.QueryContext(ctx, serviceID)
if err != nil {
return nil, err
}
defer rows.Close()
var onCall []ServiceOnCallUser
for rows.Next() {
var u ServiceOnCallUser
err = rows.Scan(&u.StepNumber, &u.UserID, &u.UserName)
if err != nil {
return nil, err
}
onCall = append(onCall, u)
}
return onCall, nil
}
// HistoryBySchedule will return the list of shifts that overlap the start and end time for the given schedule.
func (db *DB) HistoryBySchedule(ctx context.Context, scheduleID string, start, end time.Time) ([]Shift, error) {
err := permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return nil, err
}
err = validate.UUID("ScheduleID", scheduleID)
if err != nil {
return nil, err
}
tx, err := db.db.BeginTx(ctx, &sql.TxOptions{
ReadOnly: true,
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
return nil, errors.Wrap(err, "begin transaction")
}
defer tx.Rollback()
var schedTZ string
var now time.Time
err = tx.StmtContext(ctx, db.schedTZ).QueryRowContext(ctx, scheduleID).Scan(&schedTZ, &now)
if err != nil {
return nil, errors.Wrap(err, "lookup schedule time zone")
}
rows, err := tx.StmtContext(ctx, db.schedRot).QueryContext(ctx, scheduleID)
if err != nil {
return nil, errors.Wrap(err, "lookup schedule rotations")
}
defer rows.Close()
rots := make(map[string]*resolvedRotation)
var rotIDs []string
for rows.Next() {
var rot resolvedRotation
var rotTZ string
err = rows.Scan(&rot.ID, &rot.Type, &rot.Start, &rot.ShiftLength, &rotTZ, &rot.CurrentIndex, &rot.CurrentStart)
if err != nil {
return nil, errors.Wrap(err, "scan rotation info")
}
loc, err := util.LoadLocation(rotTZ)
if err != nil {
return nil, errors.Wrap(err, "load time zone info")
}
rot.Start = rot.Start.In(loc)
rots[rot.ID] = &rot
rotIDs = append(rotIDs, rot.ID)
}
rows, err = tx.StmtContext(ctx, db.rotParts).QueryContext(ctx, sqlutil.UUIDArray(rotIDs))
if err != nil {
return nil, errors.Wrap(err, "lookup rotation participants")
}
defer rows.Close()
for rows.Next() {
var rotID, userID string
err = rows.Scan(&rotID, &userID)
if err != nil {
return nil, errors.Wrap(err, "scan rotation participant info")
}
rots[rotID].Users = append(rots[rotID].Users, userID)
}
rawRules, err := db.ruleStore.FindAllTx(ctx, tx, scheduleID)
if err != nil {
return nil, errors.Wrap(err, "lookup schedule rules")
}
var rules []resolvedRule
for _, r := range rawRules {
if r.Target.TargetType() == assignment.TargetTypeRotation {
rules = append(rules, resolvedRule{
Rule: r,
Rotation: rots[r.Target.TargetID()],
})
} else {
rules = append(rules, resolvedRule{Rule: r})
}
}
rows, err = tx.StmtContext(ctx, db.schedOnCall).QueryContext(ctx, scheduleID, start, end)
if err != nil {
return nil, errors.Wrap(err, "lookup on-call history")
}
defer rows.Close()
var userHistory []Shift
for rows.Next() {
var s Shift
var end sqlutil.NullTime
err = rows.Scan(&s.UserID, &s.Start, &end)
if err != nil {
return nil, errors.Wrap(err, "scan on-call history info")
}
s.End = end.Time
userHistory = append(userHistory, s)
}
rows, err = tx.StmtContext(ctx, db.schedOverrides).QueryContext(ctx, scheduleID, start, end)
if err != nil {
return nil, errors.Wrap(err, "lookup overrides")
}
defer rows.Close()
var overrides []override.UserOverride
for rows.Next() {
var add, rem sql.NullString
var ov override.UserOverride
err = rows.Scan(&ov.Start, &ov.End, &add, &rem)
if err != nil {
return nil, errors.Wrap(err, "scan override info")
}
ov.AddUserID = add.String
ov.RemoveUserID = rem.String
overrides = append(overrides, ov)
}
err = tx.Commit()
if err != nil {
// Can't use the data we read (e.g. serialization error)
return nil, errors.Wrap(err, "commit tx")
}
tz, err := util.LoadLocation(schedTZ)
if err != nil {
return nil, errors.Wrap(err, "load time zone info")
}
s := state{
rules: rules,
overrides: overrides,
history: userHistory,
now: now,
loc: tz,
}
return s.CalculateShifts(start, end), nil
}