Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/pkg
Path: blob/main/libpkg/pkg_jobs_schedule.c
2065 views
1
/*-
2
* Copyright (c) 2024 The FreeBSD Foundation
3
*
4
* This software was developed by Isaac Freund <[email protected]>
5
* under sponsorship from the FreeBSD Foundation.
6
*
7
* Redistribution and use in source and binary forms, with or without
8
* modification, are permitted provided that the following conditions
9
* are met:
10
* 1. Redistributions of source code must retain the above copyright
11
* notice, this list of conditions and the following disclaimer
12
* in this position and unchanged.
13
* 2. Redistributions in binary form must reproduce the above copyright
14
* notice, this list of conditions and the following disclaimer in the
15
* documentation and/or other materials provided with the distribution.
16
*
17
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
18
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
21
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
*/
28
#include <assert.h>
29
30
#include "pkg.h"
31
#include "pkg/vec.h"
32
#include "private/event.h"
33
#include "private/pkg.h"
34
#include "private/pkg_jobs.h"
35
36
#define dbg(x, ...) pkg_dbg(PKG_DBG_SCHEDULER, x, __VA_ARGS__)
37
38
extern struct pkg_ctx ctx;
39
40
static const char *
41
pkg_jobs_schedule_job_type_string(struct pkg_solved *job)
42
{
43
switch (job->type) {
44
case PKG_SOLVED_INSTALL:
45
return "install";
46
case PKG_SOLVED_DELETE:
47
return "delete";
48
case PKG_SOLVED_UPGRADE:
49
return "upgrade";
50
case PKG_SOLVED_UPGRADE_INSTALL:
51
return "split upgrade install";
52
case PKG_SOLVED_UPGRADE_REMOVE:
53
return "split upgrade delete";
54
default:
55
assert(false);
56
}
57
}
58
59
/*
60
* Returns true if pkg a directly depends on pkg b.
61
*
62
* Checking only direct dependencies is sufficient to define the edges in a
63
* graph that models indirect dependencies as well as long as all of the
64
* intermediate dependencies are also nodes in the graph.
65
*/
66
static bool pkg_jobs_schedule_direct_depends(struct pkg *a, struct pkg *b)
67
{
68
struct pkg_dep *dep = NULL;
69
while (pkg_deps(a, &dep) == EPKG_OK) {
70
if (STREQ(b->uid, dep->uid)) {
71
return (true);
72
}
73
}
74
return (false);
75
}
76
77
enum pkg_jobs_schedule_graph_edge_type {
78
PKG_SCHEDULE_EDGE_NONE,
79
PKG_SCHEDULE_EDGE_NEW_DEP_NEW,
80
PKG_SCHEDULE_EDGE_OLD_DEP_OLD,
81
PKG_SCHEDULE_EDGE_OLD_CONFLICT_NEW,
82
PKG_SCHEDULE_EDGE_SPLIT_UPGRADE,
83
};
84
85
/*
86
* Jobs are nodes in a directed graph. Edges represent job scheduling order
87
* requirements. The existence of an edge from node A to node B indicates
88
* that job A must be executed before job B.
89
*
90
* There is a directed edge from node A to node B if and only if
91
* one of the following conditions holds:
92
*
93
* 1. B's new package depends on A's new package
94
* 2. A's old package depends on B's old package
95
* 3. A's old package conflicts with B's new package
96
* 4. A and B are the two halves of a split upgrade job
97
* and A is the delete half.
98
*/
99
static enum pkg_jobs_schedule_graph_edge_type
100
pkg_jobs_schedule_graph_edge(struct pkg_solved *a, struct pkg_solved *b)
101
{
102
if (a == b) {
103
return (PKG_SCHEDULE_EDGE_NONE);
104
}
105
106
if (a->xlink == b || b->xlink == a) {
107
assert(a->xlink == b && b->xlink == a);
108
assert(a->type == PKG_SOLVED_UPGRADE_INSTALL ||
109
a->type == PKG_SOLVED_UPGRADE_REMOVE);
110
assert(b->type == PKG_SOLVED_UPGRADE_INSTALL ||
111
b->type == PKG_SOLVED_UPGRADE_REMOVE);
112
assert(a->type != b->type);
113
if (a->type == PKG_SOLVED_UPGRADE_REMOVE) {
114
return (PKG_SCHEDULE_EDGE_SPLIT_UPGRADE);
115
}
116
return (PKG_SCHEDULE_EDGE_NONE);
117
}
118
119
/* TODO: These switches would be unnecessary if delete jobs used
120
* items[1] rather than items[0]. I suspect other cleanups could
121
* be made as well. */
122
struct pkg *a_new = NULL;
123
struct pkg *a_old = NULL;
124
switch (a->type) {
125
case PKG_SOLVED_INSTALL:
126
case PKG_SOLVED_UPGRADE_INSTALL:
127
a_new = a->items[0]->pkg;
128
break;
129
case PKG_SOLVED_DELETE:
130
case PKG_SOLVED_UPGRADE_REMOVE:
131
a_old = a->items[0]->pkg;
132
break;
133
case PKG_SOLVED_UPGRADE:
134
a_new = a->items[0]->pkg;
135
a_old = a->items[1]->pkg;
136
break;
137
default:
138
assert(false);
139
}
140
141
struct pkg *b_new = NULL;
142
struct pkg *b_old = NULL;
143
switch (b->type) {
144
case PKG_SOLVED_INSTALL:
145
case PKG_SOLVED_UPGRADE_INSTALL:
146
b_new = b->items[0]->pkg;
147
break;
148
case PKG_SOLVED_DELETE:
149
case PKG_SOLVED_UPGRADE_REMOVE:
150
b_old = b->items[0]->pkg;
151
break;
152
case PKG_SOLVED_UPGRADE:
153
b_new = b->items[0]->pkg;
154
b_old = b->items[1]->pkg;
155
break;
156
default:
157
assert(false);
158
}
159
160
if (a_new != NULL && b_new != NULL &&
161
pkg_jobs_schedule_direct_depends(b_new, a_new)) {
162
return (PKG_SCHEDULE_EDGE_NEW_DEP_NEW);
163
} else if (a_old != NULL && b_old != NULL &&
164
pkg_jobs_schedule_direct_depends(a_old, b_old)) {
165
return (PKG_SCHEDULE_EDGE_OLD_DEP_OLD);
166
} else if (a_old != NULL && b_new != NULL) {
167
struct pkg_conflict *conflict = NULL;
168
while (pkg_conflicts(a_old, &conflict) == EPKG_OK) {
169
if (STREQ(b_new->uid, conflict->uid)) {
170
return (PKG_SCHEDULE_EDGE_OLD_CONFLICT_NEW);
171
}
172
}
173
}
174
175
return (PKG_SCHEDULE_EDGE_NONE);
176
}
177
178
static void
179
pkg_jobs_schedule_dbg_job(pkg_solved_list *jobs, struct pkg_solved *job)
180
{
181
if (ctx.debug_level < 4) {
182
return;
183
}
184
185
dbg(4, "job: %s %s", pkg_jobs_schedule_job_type_string(job),
186
job->items[0]->pkg->uid);
187
188
vec_foreach(*jobs, i) {
189
struct pkg_solved *other = jobs->d[i];
190
if (jobs->d[i] == NULL)
191
continue;
192
switch (pkg_jobs_schedule_graph_edge(job, other)) {
193
case PKG_SCHEDULE_EDGE_NONE:
194
break;
195
case PKG_SCHEDULE_EDGE_NEW_DEP_NEW:
196
dbg(4, " edge to %s %s, new depends on new",
197
pkg_jobs_schedule_job_type_string(other),
198
other->items[0]->pkg->uid);
199
break;
200
case PKG_SCHEDULE_EDGE_OLD_DEP_OLD:
201
dbg(4, " edge to %s %s, old depends on old",
202
pkg_jobs_schedule_job_type_string(other),
203
other->items[0]->pkg->uid);
204
break;
205
case PKG_SCHEDULE_EDGE_OLD_CONFLICT_NEW:
206
dbg(4, " edge to %s %s, old conflicts with new",
207
pkg_jobs_schedule_job_type_string(other),
208
other->items[0]->pkg->uid);
209
break;
210
case PKG_SCHEDULE_EDGE_SPLIT_UPGRADE:
211
dbg(4, " edge to %s %s, split upgrade",
212
pkg_jobs_schedule_job_type_string(other),
213
other->items[0]->pkg->uid);
214
break;
215
default:
216
assert(false);
217
}
218
}
219
}
220
221
static bool
222
pkg_jobs_schedule_has_incoming_edge(pkg_solved_list *nodes,
223
struct pkg_solved *node)
224
{
225
vec_foreach(*nodes, i) {
226
if (pkg_jobs_schedule_graph_edge(nodes->d[i], node) != PKG_SCHEDULE_EDGE_NONE) {
227
return (true);
228
}
229
}
230
return (false);
231
}
232
233
/*
234
* Prioritizing the install jobs and deprioritizing the delete jobs of split
235
* upgrades reduces the distance between the two halves of the split job in the
236
* final execution order.
237
*/
238
static int
239
pkg_jobs_schedule_priority(struct pkg_solved *node)
240
{
241
switch (node->type) {
242
case PKG_SOLVED_UPGRADE_INSTALL:
243
return 1;
244
case PKG_SOLVED_UPGRADE_REMOVE:
245
return -1;
246
default:
247
return 0;
248
}
249
}
250
251
/* This comparison function is used as a tiebreaker in the topological sort. */
252
static int
253
pkg_jobs_schedule_cmp_available(struct pkg_solved *a, struct pkg_solved *b)
254
{
255
int ret = pkg_jobs_schedule_priority(a) - pkg_jobs_schedule_priority(b);
256
if (ret == 0) {
257
/* Falling back to lexicographical ordering ensures that job execution
258
* order is always consistent and makes testing easier. */
259
return strcmp(b->items[0]->pkg->uid, a->items[0]->pkg->uid);
260
} else {
261
return ret;
262
}
263
}
264
265
/*
266
* Move job nodes with no incoming edges from unavailable to available.
267
* If changed is non-NULL, only check jobs with an incoming edge from changed. */
268
static void
269
pkg_jobs_schedule_update_available(pkg_solved_list *unavailable,
270
pkg_solved_list *available, struct pkg_solved *changed)
271
{
272
for (size_t i = 0; i < unavailable->len;) {
273
struct pkg_solved *job = unavailable->d[i];
274
if ((changed == NULL ||
275
pkg_jobs_schedule_graph_edge(changed, job) != PKG_SCHEDULE_EDGE_NONE) &&
276
!pkg_jobs_schedule_has_incoming_edge(unavailable, job) &&
277
!pkg_jobs_schedule_has_incoming_edge(available, job)) {
278
vec_push(available, job);
279
vec_swap_remove(unavailable, i);
280
} else {
281
i++;
282
}
283
}
284
}
285
286
/* Topological sort based on Kahn's algorithm with a tiebreaker */
287
static void
288
pkg_jobs_schedule_topological_sort(pkg_solved_list *jobs)
289
{
290
pkg_solved_list unavailable = *jobs;
291
pkg_solved_list available = vec_init();
292
*jobs = (pkg_solved_list)vec_init();
293
294
pkg_jobs_schedule_update_available(&unavailable, &available, NULL);
295
296
while (available.len > 0) {
297
/* Add the highest priority job from the set of available jobs
298
* to the sorted list */
299
size_t max = 0;
300
for (size_t i = 1; i < available.len; i++) {
301
if (pkg_jobs_schedule_cmp_available(available.d[i], available.d[max]) > 0) {
302
max = i;
303
}
304
}
305
struct pkg_solved *node = available.d[max];
306
vec_push(jobs, node);
307
vec_swap_remove(&available, max);
308
309
/* Again, place all job nodes with no incoming edges in the set
310
* of available jobs, ignoring any incoming edges from job nodes
311
* already added to the sorted list */
312
pkg_jobs_schedule_update_available(&unavailable, &available, node);
313
}
314
315
/* The unavailable set will only be non-empty at this point if there is a
316
* cycle in the graph and all cycles must be eliminated by splitting
317
* upgrade jobs before calling this function. */
318
assert(unavailable.len == 0);
319
320
vec_free(&unavailable);
321
vec_free(&available);
322
}
323
324
/*
325
* This is a depth-first search that keeps track of the path taken to the
326
* current node in the graph. If a node on this path is encountered a
327
* second time a cycle has been found.
328
*/
329
static struct pkg_solved *
330
pkg_jobs_schedule_find_cycle(pkg_solved_list *jobs,
331
struct pkg_solved **path, struct pkg_solved *node)
332
{
333
/* Push node to path */
334
assert(node->mark == PKG_SOLVED_CYCLE_MARK_NONE);
335
node->mark = PKG_SOLVED_CYCLE_MARK_PATH;
336
assert(node->path_prev == NULL);
337
node->path_prev = *path;
338
*path = node;
339
340
vec_foreach(*jobs, i) {
341
if (pkg_jobs_schedule_graph_edge(node, jobs->d[i]) != PKG_SCHEDULE_EDGE_NONE) {
342
switch (jobs->d[i]->mark){
343
case PKG_SOLVED_CYCLE_MARK_NONE:;
344
struct pkg_solved *cycle =
345
pkg_jobs_schedule_find_cycle(jobs, path, jobs->d[i]);
346
if (cycle != NULL) {
347
return (cycle);
348
}
349
break;
350
case PKG_SOLVED_CYCLE_MARK_DONE:
351
break;
352
case PKG_SOLVED_CYCLE_MARK_PATH:
353
return (jobs->d[i]); /* Found a cycle */
354
default:
355
assert(false);
356
}
357
}
358
}
359
360
/* Pop node from path */
361
assert(node->mark == PKG_SOLVED_CYCLE_MARK_PATH);
362
node->mark = PKG_SOLVED_CYCLE_MARK_DONE;
363
*path = node->path_prev;
364
node->path_prev = NULL;
365
366
return (NULL);
367
}
368
369
int pkg_jobs_schedule(struct pkg_jobs *j)
370
{
371
while (true) {
372
dbg(3, "checking job scheduling graph for cycles...");
373
374
vec_foreach(j->jobs, i) {
375
j->jobs.d[i]->mark = PKG_SOLVED_CYCLE_MARK_NONE;
376
j->jobs.d[i]->path_prev = NULL;
377
378
pkg_jobs_schedule_dbg_job(&j->jobs, j->jobs.d[i]);
379
}
380
381
/* The graph may not be connected, in which case it is necessary to
382
* run multiple searches for cycles from different start nodes. */
383
struct pkg_solved *path = NULL;
384
struct pkg_solved *cycle = NULL;
385
vec_foreach(j->jobs, i) {
386
switch (j->jobs.d[i]->mark) {
387
case PKG_SOLVED_CYCLE_MARK_NONE:
388
cycle = pkg_jobs_schedule_find_cycle(&j->jobs, &path, j->jobs.d[i]);
389
break;
390
case PKG_SOLVED_CYCLE_MARK_DONE:
391
break;
392
case PKG_SOLVED_CYCLE_MARK_PATH:
393
default:
394
assert(false);
395
}
396
if (cycle != NULL) {
397
break;
398
}
399
}
400
401
if (cycle == NULL) {
402
dbg(3, "no job scheduling graph cycles found");
403
assert(path == NULL);
404
break;
405
}
406
407
dbg(3, "job scheduling graph cycle found");
408
assert(path != NULL);
409
assert(path->path_prev != NULL);
410
assert(path != cycle);
411
412
/* Close the path into a cycle to eliminate some edge cases in the loop. */
413
cycle->path_prev = path;
414
415
/* Not all upgrade jobs would break the cycle if split.
416
* It is helpful to think of each upgrade job as two separate
417
* nodes, the remove half and the install half. If a cycle only
418
* involved one half of the upgrade job, splitting the upgrade
419
* job would not break the cycle or even change its length.
420
*
421
* Furthermore, since there is an edge from the remove half of
422
* an upgrade job to the install half, the install half must
423
* come *before* the remove half in the cycle. Otherwise
424
* splitting the upgrade job will only make the cycle one node
425
* longer.
426
*/
427
enum pkg_jobs_schedule_graph_edge_type out =
428
pkg_jobs_schedule_graph_edge(path, cycle);
429
assert(out != PKG_SCHEDULE_EDGE_NONE);
430
enum pkg_jobs_schedule_graph_edge_type in =
431
pkg_jobs_schedule_graph_edge(path->path_prev, path);
432
assert(in != PKG_SCHEDULE_EDGE_NONE);
433
while (true) {
434
if (path->type == PKG_SOLVED_UPGRADE) {
435
assert(out != PKG_SCHEDULE_EDGE_SPLIT_UPGRADE);
436
assert(in != PKG_SCHEDULE_EDGE_SPLIT_UPGRADE);
437
if ((out == PKG_SCHEDULE_EDGE_OLD_DEP_OLD ||
438
out == PKG_SCHEDULE_EDGE_OLD_CONFLICT_NEW) &&
439
(in == PKG_SCHEDULE_EDGE_NEW_DEP_NEW ||
440
in == PKG_SCHEDULE_EDGE_OLD_CONFLICT_NEW)) {
441
/* Found an upgrade job that would break
442
* the cycle if split. */
443
break;
444
}
445
}
446
if (path == cycle) {
447
pkg_emit_error("failed to break job scheduling cycle");
448
return (EPKG_FATAL);
449
}
450
path = path->path_prev;
451
assert(path != NULL);
452
out = in;
453
in = pkg_jobs_schedule_graph_edge(path->path_prev, path);
454
assert(in != PKG_SCHEDULE_EDGE_NONE);
455
}
456
457
/* path is now the upgrade job chosen to be split */
458
dbg(2, "splitting upgrade %s job", path->items[0]->pkg->uid);
459
460
struct pkg_solved *new = xcalloc(1, sizeof(struct pkg_solved));
461
new->type = PKG_SOLVED_UPGRADE_REMOVE;
462
new->items[0] = path->items[1];
463
new->xlink = path;
464
path->type = PKG_SOLVED_UPGRADE_INSTALL;
465
path->items[1] = NULL;
466
path->xlink = new;
467
vec_push(&j->jobs, new);
468
}
469
470
pkg_jobs_schedule_topological_sort(&j->jobs);
471
472
dbg(3, "finished job scheduling");
473
474
return (EPKG_OK);
475
}
476
477