@artian-techs/adonis-orbit 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,12 @@ import type { OrbitConfig } from './types.js';
9
9
  * The channels of the queue answer at once, thus the events are written apart.
10
10
  * A sink that refuses an event must never stop the job that runs.
11
11
  *
12
+ * The start and the end of a short job come in the same tick, and a sink
13
+ * answers each write apart. The end would land before the start, and the row
14
+ * would stay open. The events of one job thus go to one sink one after the
15
+ * other, in the order that the queue gave them. Two jobs do not wait for each
16
+ * other, and two sinks do not wait for each other.
17
+ *
12
18
  * A configuration that names no sink keeps nothing.
13
19
  *
14
20
  * The recorder gives back a `flush`. It waits for the writes that are open,
@@ -46,6 +46,12 @@ function keep(rule, job, payload) {
46
46
  * The channels of the queue answer at once, thus the events are written apart.
47
47
  * A sink that refuses an event must never stop the job that runs.
48
48
  *
49
+ * The start and the end of a short job come in the same tick, and a sink
50
+ * answers each write apart. The end would land before the start, and the row
51
+ * would stay open. The events of one job thus go to one sink one after the
52
+ * other, in the order that the queue gave them. Two jobs do not wait for each
53
+ * other, and two sinks do not wait for each other.
54
+ *
49
55
  * A configuration that names no sink keeps nothing.
50
56
  *
51
57
  * The recorder gives back a `flush`. It waits for the writes that are open,
@@ -56,13 +62,30 @@ export function recordJobRuns(config) {
56
62
  const rule = config.payload ?? 'full';
57
63
  /** The writes that are started, and that no sink finished yet. */
58
64
  const pending = new Set();
65
+ /** The last write of each job that a sink did not finish yet, by sink. */
66
+ const lanes = new Map(sinks.map((sink) => [sink, new Map()]));
59
67
  function write(events) {
68
+ const ids = [...new Set(events.map((event) => event.job.id))];
60
69
  for (const sink of sinks) {
61
- const done = sink.write(events).catch((error) => {
70
+ const lane = lanes.get(sink);
71
+ const before = Promise.all(ids.map((id) => lane.get(id)));
72
+ const done = before
73
+ .then(() => sink.write(events))
74
+ .catch((error) => {
62
75
  logger.error({ err: error }, 'The trace of a job could not be written');
63
76
  });
77
+ for (const id of ids) {
78
+ lane.set(id, done);
79
+ }
64
80
  pending.add(done);
65
- void done.finally(() => pending.delete(done));
81
+ void done.finally(() => {
82
+ pending.delete(done);
83
+ for (const id of ids) {
84
+ if (lane.get(id) === done) {
85
+ lane.delete(id);
86
+ }
87
+ }
88
+ });
66
89
  }
67
90
  }
68
91
  /**
@@ -2,13 +2,18 @@ import type { JobEvent, Sink } from '../types.js';
2
2
  /**
3
3
  * Keep the trace of the jobs in the table of the application.
4
4
  *
5
- * The two inserts are written in SQL because they need `on conflict`, which
5
+ * The three writes are written in SQL because they need `on conflict`, which
6
6
  * Lucid does not carry. The model would read the row before writing it, and
7
7
  * two processes that write together would then undo each other.
8
8
  *
9
- * The row that the dispatch writes and the row that the worker writes are the
10
- * same row. Either of the two can arrive first, thus the first one writes and
11
- * the second one completes.
9
+ * The three events of one job write the same row, and each one can land
10
+ * first: the dispatch and the worker run in two processes, and a worker that
11
+ * stops before its writes end leaves the row to the reconciler. The first
12
+ * event writes the row, and the next ones complete it.
13
+ *
14
+ * A row that is closed stays closed. A start that lands after the end fills
15
+ * what the end did not know, and changes nothing else. A `lost` that lands
16
+ * after the end changes nothing: the row has its answer.
12
17
  */
13
18
  export default class DatabaseSink implements Sink {
14
19
  #private;
@@ -7,16 +7,25 @@ const STATUS = {
7
7
  retrying: JobRunStatus.RETRYING,
8
8
  lost: JobRunStatus.LOST,
9
9
  };
10
+ /** The statuses of a row that waits for an answer. */
11
+ const OPEN = [JobRunStatus.PENDING, JobRunStatus.RUNNING, JobRunStatus.RETRYING];
12
+ /** `job_runs.status in (?, ?, ?)`, with one mark for each open status. */
13
+ const IS_OPEN = `${JobRun.table}.status in (${OPEN.map(() => '?').join(', ')})`;
10
14
  /**
11
15
  * Keep the trace of the jobs in the table of the application.
12
16
  *
13
- * The two inserts are written in SQL because they need `on conflict`, which
17
+ * The three writes are written in SQL because they need `on conflict`, which
14
18
  * Lucid does not carry. The model would read the row before writing it, and
15
19
  * two processes that write together would then undo each other.
16
20
  *
17
- * The row that the dispatch writes and the row that the worker writes are the
18
- * same row. Either of the two can arrive first, thus the first one writes and
19
- * the second one completes.
21
+ * The three events of one job write the same row, and each one can land
22
+ * first: the dispatch and the worker run in two processes, and a worker that
23
+ * stops before its writes end leaves the row to the reconciler. The first
24
+ * event writes the row, and the next ones complete it.
25
+ *
26
+ * A row that is closed stays closed. A start that lands after the end fills
27
+ * what the end did not know, and changes nothing else. A `lost` that lands
28
+ * after the end changes nothing: the row has its answer.
20
29
  */
21
30
  export default class DatabaseSink {
22
31
  async write(events) {
@@ -52,15 +61,24 @@ export default class DatabaseSink {
52
61
  values ${rows}
53
62
  on conflict (job_id) do nothing`, values);
54
63
  }
55
- /** Write the row of the job that a worker holds, or complete it. */
64
+ /**
65
+ * Write the row of the job that a worker holds, or complete it.
66
+ *
67
+ * An open row takes the start: the status, the count of the tries and the
68
+ * time of this try. A closed row keeps its status, and takes the time of the
69
+ * start only while it has none.
70
+ */
56
71
  #take({ job, at }) {
57
72
  return db.rawQuery(`insert into ${JobRun.table}
58
73
  (job_id, name, queue, status, payload, attempts, started_at, created_at, updated_at)
59
74
  values (?, ?, ?, ?, ?, ?, ?, ?, ?)
60
75
  on conflict (job_id) do update set
61
- status = excluded.status,
62
- attempts = excluded.attempts,
63
- started_at = excluded.started_at,
76
+ status = case when ${IS_OPEN} then excluded.status else ${JobRun.table}.status end,
77
+ attempts = greatest(${JobRun.table}.attempts, excluded.attempts),
78
+ started_at = case when ${IS_OPEN}
79
+ then excluded.started_at
80
+ else coalesce(${JobRun.table}.started_at, excluded.started_at)
81
+ end,
64
82
  updated_at = excluded.updated_at`, [
65
83
  job.id,
66
84
  job.name,
@@ -71,19 +89,43 @@ export default class DatabaseSink {
71
89
  at,
72
90
  at,
73
91
  at,
92
+ ...OPEN,
93
+ ...OPEN,
74
94
  ]);
75
95
  }
76
- /** Say how the try of a job ended. */
96
+ /**
97
+ * Say how the try of a job ended.
98
+ *
99
+ * The end can land before the start, thus it writes the row when there is
100
+ * none. A `lost` says that nobody answered, thus it writes nothing on a row
101
+ * that has an answer.
102
+ */
77
103
  #close({ job, at, outcome, duration, error }) {
78
104
  const ended = outcome !== 'retrying';
79
- return JobRun.query()
80
- .where('jobId', job.id)
81
- .update({
82
- status: STATUS[outcome],
105
+ return db.rawQuery(`insert into ${JobRun.table}
106
+ (job_id, name, queue, status, payload, attempts, duration, error, finished_at, created_at, updated_at)
107
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
108
+ on conflict (job_id) do update set
109
+ status = excluded.status,
110
+ attempts = greatest(${JobRun.table}.attempts, excluded.attempts),
111
+ duration = excluded.duration,
112
+ error = excluded.error,
113
+ finished_at = excluded.finished_at,
114
+ updated_at = excluded.updated_at
115
+ where excluded.status <> ? or ${IS_OPEN}`, [
116
+ job.id,
117
+ job.name,
118
+ job.queue,
119
+ STATUS[outcome],
120
+ JSON.stringify(job.payload),
121
+ job.attempts,
83
122
  duration,
84
123
  error,
85
- finishedAt: ended ? at : null,
86
- updatedAt: at,
87
- });
124
+ ended ? at : null,
125
+ at,
126
+ at,
127
+ JobRunStatus.LOST,
128
+ ...OPEN,
129
+ ]);
88
130
  }
89
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artian-techs/adonis-orbit",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "See what the queue of AdonisJS does, keep it, and ask a job again.",
6
6
  "main": "build/index.js",