@evanp/activitypub-bot 0.32.0 → 0.32.2

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.
@@ -108,7 +108,11 @@ export class ActivityPubClient {
108
108
  if (res.status < 200 || res.status > 299) {
109
109
  const body = await res.text()
110
110
  this.#logger.warn({ status: res.status, body, url }, 'Could not fetch url')
111
- throw createHttpError(res.status, `Could not fetch ${url}`)
111
+ throw createHttpError(
112
+ res.status,
113
+ `Could not fetch ${url}`,
114
+ { headers: res.headers }
115
+ )
112
116
  }
113
117
  const contentType = res.headers.get('content-type')
114
118
  const mimeType = contentType?.split(';')[0].trim()
@@ -172,7 +176,11 @@ export class ActivityPubClient {
172
176
  await this.#limiter.update(hostname, res.headers)
173
177
  this.#logger.debug(`Done fetching POST for ${url}`)
174
178
  if (res.status < 200 || res.status > 299) {
175
- throw createHttpError(res.status, await res.text())
179
+ throw createHttpError(
180
+ res.status,
181
+ await res.text(),
182
+ { headers: res.headers }
183
+ )
176
184
  }
177
185
  }
178
186
 
@@ -4,7 +4,7 @@ import as2 from './activitystreams.js'
4
4
 
5
5
  export class DistributionWorker {
6
6
  static #QUEUE_ID = 'distribution'
7
- static #MAX_ATTEMPTS = 16
7
+ static #MAX_ATTEMPTS = 21 // ~24 days
8
8
  #jobQueue
9
9
  #client
10
10
  #logger
@@ -43,21 +43,64 @@ export class DistributionWorker {
43
43
  this.#logger.info({ jobId }, 'completed job')
44
44
  } catch (error) {
45
45
  if (!error.status) {
46
- this.#logger.error(`Could not deliver ${activity.id} to ${inbox}: ${error.message}`)
47
- this.#logger.error(error.stack)
46
+ this.#logger.warn(
47
+ { error, activity: activity.id, inbox },
48
+ 'Could not deliver activity and no HTTP status available')
49
+ await this.#jobQueue.fail(jobId, this.#workerId)
48
50
  } else if (error.status >= 300 && error.status < 400) {
49
- this.#logger.error(`Unexpected redirect code delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}`)
51
+ this.#logger.warn(
52
+ { error, activity: activity.id, inbox },
53
+ 'Could not deliver activity and unexpected redirect code'
54
+ )
55
+ await this.#jobQueue.fail(jobId, this.#workerId)
50
56
  } else if (error.status >= 400 && error.status < 500) {
51
- this.#logger.error(`Bad request delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}`)
57
+ this.#logger.warn(
58
+ { error, activity: activity.id, inbox },
59
+ 'Could not deliver activity due to client error'
60
+ )
61
+ if (error.status === 429) {
62
+ this.#logger.debug(
63
+ { error, activity: activity.id, inbox },
64
+ 'Retrying on 429 status'
65
+ )
66
+ let delay
67
+ if (error.headers && error.headers['retry-after']) {
68
+ this.#logger.debug('using retry-after header')
69
+ const retryAfter = error.headers['retry-after']
70
+ if (/^\d+$/.test(retryAfter)) {
71
+ delay = parseInt(retryAfter, 10) * 1000
72
+ } else {
73
+ delay = new Date(retryAfter) - Date.now()
74
+ }
75
+ } else {
76
+ this.#logger.debug('exponential backoff')
77
+ delay = Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
78
+ }
79
+ await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
80
+ } else {
81
+ await this.#jobQueue.fail(jobId, this.#workerId)
82
+ }
52
83
  } else if (error.status >= 500 && error.status < 600) {
53
84
  if (attempts >= DistributionWorker.#MAX_ATTEMPTS) {
54
- this.#logger.error(`Server error delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}; giving up after ${attempts} attempts`)
55
- await this.#jobQueue.complete(jobId, this.#workerId)
85
+ this.#logger.warn(
86
+ { error, activity: activity.id, inbox, attempts },
87
+ 'Could not deliver activity due to server error; no more attempts'
88
+ )
89
+ await this.#jobQueue.fail(jobId, this.#workerId)
56
90
  } else {
57
91
  const delay = Math.round((2 ** (attempts - 1) * 1000) * (0.5 + Math.random()))
58
- this.#logger.warn(`Server error delivering ${activity.id} to ${inbox}: ${error.status} ${error.message}; will retry in ${delay} ms (${attempts} of ${DistributionWorker.#MAX_ATTEMPTS})`)
92
+ this.#logger.warn(
93
+ { error, activity: activity.id, inbox, attempts, delay },
94
+ 'Could not deliver activity due to server error; will retry'
95
+ )
59
96
  await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
60
97
  }
98
+ } else {
99
+ this.#logger.warn(
100
+ { error, activity: activity.id, inbox },
101
+ 'Could not deliver activity due to unexpected status range'
102
+ )
103
+ await this.#jobQueue.fail(jobId, this.#workerId)
61
104
  }
62
105
  }
63
106
  } catch (err) {
@@ -67,7 +110,12 @@ export class DistributionWorker {
67
110
  }
68
111
  this.#logger.warn({ err, jobId }, 'Error delivering to bot')
69
112
  if (jobId) {
70
- await this.#jobQueue.release(jobId, this.#workerId)
113
+ const delay = Math.round((2 ** ((attempts ?? 1) - 1) * 1000) * (0.5 + Math.random()))
114
+ this.#logger.warn(
115
+ { err, jobId, attempts, delay },
116
+ 'Retrying job after a delay'
117
+ )
118
+ await this.#jobQueue.retryAfter(jobId, this.#workerId, delay)
71
119
  }
72
120
  }
73
121
  }
package/lib/jobqueue.js CHANGED
@@ -157,6 +157,22 @@ export class JobQueue {
157
157
  this.#ac.abort()
158
158
  }
159
159
 
160
+ async fail (jobId, jobRunnerId) {
161
+ await this.#connection.query(`
162
+ INSERT INTO failed_job
163
+ (job_id, queue_id, priority, payload, claimed_at, claimed_by, attempts, retry_after, created_at, updated_at)
164
+ SELECT job_id, queue_id, priority, payload, claimed_at, claimed_by, attempts, retry_after, created_at, updated_at
165
+ FROM job
166
+ WHERE job_id = ?
167
+ AND claimed_by = ?;`,
168
+ { replacements: [jobId, jobRunnerId] })
169
+ await this.#connection.query(`
170
+ DELETE FROM job
171
+ WHERE job_id = ? AND claimed_by = ?;`,
172
+ { replacements: [jobId, jobRunnerId] })
173
+ this.#logger.debug({ method: 'complete', jobRunnerId, jobId }, 'completed job')
174
+ }
175
+
160
176
  async #countJobs (queueId) {
161
177
  this.#logger.debug({ method: '#countJobs', queueId }, 'checking queue size')
162
178
  const rows = await this.#connection.query(`
@@ -0,0 +1,21 @@
1
+ export const id = '006-failed-jobs'
2
+
3
+ export async function up (connection, queryOptions = {}) {
4
+ await connection.query(`
5
+ CREATE TABLE failed_job (
6
+ job_id char(21) NOT NULL PRIMARY KEY,
7
+ queue_id varchar(64) NOT NULL,
8
+ priority INTEGER,
9
+ payload TEXT,
10
+ claimed_at TIMESTAMP,
11
+ claimed_by varchar(64),
12
+ attempts INTEGER default 0,
13
+ retry_after TIMESTAMP,
14
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
15
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
16
+ );
17
+ `, queryOptions)
18
+ await connection.query(`
19
+ CREATE INDEX failed_job_queue_id on job (queue_id);
20
+ `, queryOptions)
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.32.0",
3
+ "version": "0.32.2",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",