Skip to content

Commit 6aa275e

Browse files
committed
feat(export): consolidate on using shared package
1 parent 81de2f0 commit 6aa275e

39 files changed

+384
-1487
lines changed

packages/aws-utils/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"@aws-sdk/client-sqs": "3.716.0",
9393
"@aws-sdk/lib-storage": "3.716.0",
9494
"@aws-sdk/s3-request-presigner": "3.716.0",
95+
"@opentelemetry/api": "1.9.0",
9596
"@pocket-tools/event-bridge": "workspace:*",
9697
"@pocket-tools/ts-logger": "workspace:*",
9798
"@sentry/node": "8.47.0",
@@ -103,6 +104,7 @@
103104
"devDependencies": {
104105
"@pocket-tools/eslint-config": "workspace:*",
105106
"@semantic-release/exec": "6.0.3",
107+
"@sentry/core": "^9.3.0",
106108
"@smithy/types": "3.5.0",
107109
"@types/adm-zip": "0.5.7",
108110
"@types/archiver": "^6.0.3",
@@ -117,7 +119,8 @@
117119
"ts-node": "10.9.2",
118120
"tsconfig": "workspace:*",
119121
"tsup": "8.3.5",
120-
"typescript": "5.7.3"
122+
"typescript": "5.7.3",
123+
"unleash-client": "6.1.2"
121124
},
122125
"publishConfig": {
123126
"access": "public"

packages/aws-utils/src/AsyncDataExportService.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class ExportConcrete extends AsyncDataExportService<
4545
return Promise.resolve([]);
4646
}
4747

48-
formatExport(entries: ExportRecord[]) {
48+
async formatExport(entries: ExportRecord[]) {
4949
return entries;
5050
}
5151

packages/aws-utils/src/AsyncDataExportService.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ export abstract class AsyncDataExportService<I extends { cursor: number }, O> {
4444
return path.join(this.s3.partsPrefix, this.user.encodedId);
4545
}
4646

47-
abstract fileKey(part: number): string;
47+
abstract fileKey(identifier: number | string): string;
4848

4949
abstract fetchData(from: number, size: number): Promise<Array<I>>;
5050

51-
abstract formatExport(entries: I[]): O[];
51+
abstract formatExport(entries: I[]): Promise<O[]>;
5252

5353
abstract write(records: O[], fileKey: string): Promise<void>;
5454

@@ -89,7 +89,7 @@ export abstract class AsyncDataExportService<I extends { cursor: number }, O> {
8989
}
9090
// We're finished!
9191
else if (entries.length <= size) {
92-
const records = this.formatExport(entries);
92+
const records = await this.formatExport(entries);
9393
await this.write(records, this.fileKey(part));
9494
await this.notifyComplete(
9595
this.user.encodedId,
@@ -100,7 +100,7 @@ export abstract class AsyncDataExportService<I extends { cursor: number }, O> {
100100
// Will pull in greater than or equal to cursor
101101
// Updates array in-place to remove the record
102102
const cursor = entries.splice(size)[0].cursor;
103-
const records = this.formatExport(entries);
103+
const records = await this.formatExport(entries);
104104
await this.write(records, this.fileKey(part));
105105
serverLogger.info({
106106
message: `DataExportService::${this.serviceName} - Requesting next chunk`,

servers/account-data-deleter/src/queueHandlers/queueHandler.ts packages/aws-utils/src/QueuePoller.ts

+55-40
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,25 @@ import {
77
ReceiveMessageCommandOutput,
88
} from '@aws-sdk/client-sqs';
99
import * as Sentry from '@sentry/node';
10-
import { config } from '../config';
11-
import { SqsMessage } from '../routes/queueDelete';
1210
import { setTimeout } from 'timers/promises';
13-
import { SeverityLevel } from '@sentry/types';
14-
import { unleash } from '../unleash';
1511
import { type Unleash } from 'unleash-client';
1612
import { serverLogger } from '@pocket-tools/ts-logger';
17-
import { QueueConfig } from '../types';
1813
import * as otel from '@opentelemetry/api';
1914

20-
export abstract class QueueHandler {
21-
readonly sqsClient: SQSClient;
15+
export interface QueueConfig {
16+
batchSize: number;
17+
url: string;
18+
visibilityTimeout: number;
19+
maxMessages: number;
20+
waitTimeSeconds: number;
21+
defaultPollIntervalSeconds: number;
22+
afterMessagePollIntervalSeconds: number;
23+
messageRetentionSeconds: number;
24+
name: string;
25+
}
26+
27+
export abstract class QueuePoller<TMessageBody> {
2228
private tracer: otel.Tracer;
23-
protected unleashClient: Unleash;
2429

2530
/**
2631
* Class for deleting records in batches from the database,
@@ -43,23 +48,26 @@ export abstract class QueueHandler {
4348
* in the future.
4449
*/
4550
constructor(
46-
public readonly emitter: EventEmitter,
47-
public readonly eventName: string,
48-
public readonly queueConfig: QueueConfig,
49-
pollOnInit = true,
50-
unleashClient?: Unleash,
51+
protected events: {
52+
emitter: EventEmitter;
53+
eventName: string;
54+
},
55+
protected sqs: {
56+
config: QueueConfig;
57+
client: SQSClient;
58+
},
59+
protected opts: {
60+
pollOnInit?: boolean;
61+
},
5162
) {
52-
this.sqsClient = new SQSClient({
53-
region: config.aws.region,
54-
endpoint: config.aws.endpoint,
55-
maxAttempts: 3,
56-
});
57-
this.unleashClient = unleashClient ?? unleash();
63+
// Default to polling when class is instantiated (really only when
64+
// testing do you want it otherwise)
65+
const pollOnInit = opts.pollOnInit != null ? opts.pollOnInit : true;
5866
this.tracer = otel.trace.getTracer('queue-tracer');
59-
emitter.on(this.eventName, async () => await this.pollQueue());
67+
events.emitter.on(events.eventName, async () => await this.pollQueue());
6068
// Start the polling by emitting an initial event
6169
if (pollOnInit) {
62-
emitter.emit(this.eventName);
70+
events.emitter.emit(events.eventName);
6371
}
6472
}
6573

@@ -70,20 +78,22 @@ export abstract class QueueHandler {
7078
*/
7179
async deleteMessage(message: Message) {
7280
const deleteParams = {
73-
QueueUrl: this.queueConfig.url,
81+
QueueUrl: this.sqs.config.url,
7482
ReceiptHandle: message.ReceiptHandle,
7583
};
7684
try {
77-
await this.sqsClient.send(new DeleteMessageCommand(deleteParams));
85+
await this.sqs.client.send(new DeleteMessageCommand(deleteParams));
7886
} catch (error) {
7987
const errorMessage = 'Error deleting message from queue';
8088
serverLogger.error({
8189
message: errorMessage,
8290
error: error,
8391
errorData: message,
92+
queue: this.sqs.config.url,
93+
});
94+
Sentry.captureException(error, {
95+
data: { ...message, queue: this.sqs.config.url },
8496
});
85-
Sentry.addBreadcrumb({ message: errorMessage, data: message });
86-
Sentry.captureException(error);
8797
}
8898
}
8999

@@ -95,7 +105,7 @@ export abstract class QueueHandler {
95105
* @returns whether or not the message was successfully handled
96106
* (underlying call to AccountDeleteDataService completed without error)
97107
*/
98-
abstract handleMessage(body: object): Promise<boolean>;
108+
abstract handleMessage(body: TMessageBody): Promise<boolean>;
99109
/**
100110
* Set a timeout to emit another poll event which will be handled
101111
* by the listener.
@@ -106,7 +116,7 @@ export abstract class QueueHandler {
106116
serverLogger.info(`Set next poll timeout at ${timeout}`);
107117
await setTimeout(timeout);
108118
}
109-
this.emitter.emit(this.eventName);
119+
this.events.emitter.emit(this.events.eventName);
110120
}
111121

112122
/**
@@ -118,7 +128,7 @@ export abstract class QueueHandler {
118128
async pollQueue() {
119129
return await Sentry.withIsolationScope(async () => {
120130
return await this.tracer.startActiveSpan(
121-
`poll-queue-${this.queueConfig.name}`,
131+
`poll-queue-${this.sqs.config.name}`,
122132
{ root: true },
123133
async (span: otel.Span) => {
124134
await this.__pollQueue(span);
@@ -140,20 +150,20 @@ export abstract class QueueHandler {
140150
const params = {
141151
// https://github.com/aws/aws-sdk/issues/233
142152
AttributeNames: ['SentTimestamp'] as any, // see issue above - bug in the SDK
143-
MaxNumberOfMessages: this.queueConfig.maxMessages,
153+
MaxNumberOfMessages: this.sqs.config.maxMessages,
144154
MessageAttributeNames: ['All'],
145-
QueueUrl: this.queueConfig.url,
146-
VisibilityTimeout: this.queueConfig.visibilityTimeout,
147-
WaitTimeSeconds: this.queueConfig.waitTimeSeconds,
155+
QueueUrl: this.sqs.config.url,
156+
VisibilityTimeout: this.sqs.config.visibilityTimeout,
157+
WaitTimeSeconds: this.sqs.config.waitTimeSeconds,
148158
};
149159

150-
serverLogger.info(`Begining polling of ${this.queueConfig.url}`);
160+
serverLogger.info(`Begining polling of ${this.sqs.config.url}`);
151161

152162
let data: ReceiveMessageCommandOutput | null = null;
153-
let body: SqsMessage | null = null;
163+
let body: TMessageBody | null = null;
154164

155165
try {
156-
data = await this.sqsClient.send(new ReceiveMessageCommand(params));
166+
data = await this.sqs.client.send(new ReceiveMessageCommand(params));
157167
body =
158168
data.Messages &&
159169
data.Messages.length > 0 &&
@@ -162,9 +172,14 @@ export abstract class QueueHandler {
162172
: null;
163173
} catch (error) {
164174
const receiveError = 'PollQueue: Error receiving messages from queue';
165-
serverLogger.error({ message: receiveError, error: error });
166-
Sentry.addBreadcrumb({ message: receiveError });
167-
Sentry.captureException(error, { level: 'fatal' as SeverityLevel });
175+
serverLogger.error({
176+
message: receiveError,
177+
error: error,
178+
queue: this.sqs.config.url,
179+
});
180+
Sentry.captureException(error, {
181+
data: { queue: this.sqs.config.url },
182+
});
168183
span.recordException(error);
169184
span.setStatus({ code: otel.SpanStatusCode.ERROR });
170185
}
@@ -176,12 +191,12 @@ export abstract class QueueHandler {
176191
}
177192
// Schedule next message poll
178193
await this.scheduleNextPoll(
179-
this.queueConfig.afterMessagePollIntervalSeconds * 1000,
194+
this.sqs.config.afterMessagePollIntervalSeconds * 1000,
180195
);
181196
} else {
182197
// If no messages were found, schedule another poll after a short time
183198
await this.scheduleNextPoll(
184-
this.queueConfig.defaultPollIntervalSeconds * 1000,
199+
this.sqs.config.defaultPollIntervalSeconds * 1000,
185200
);
186201
}
187202
}

packages/aws-utils/src/S3Bucket.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,10 @@ export class S3Bucket {
236236
});
237237
if (credentials) {
238238
const assumedS3 = new S3Client({
239-
endpoint: config.aws.endpoint,
240-
region: config.aws.region,
239+
endpoint: this.awsConfig?.endpoint,
240+
region: this.awsConfig?.region,
241241
maxAttempts: 3,
242-
forcePathStyle: config.aws.endpoint != null ? true : false,
242+
forcePathStyle: this.awsConfig?.endpoint != null ? true : false,
243243
credentials,
244244
});
245245
return await getSignedUrl(assumedS3, command, { expiresIn });

packages/aws-utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './AsyncDataExportService.ts';
22
export * from './S3Bucket.ts';
33
export * from './s3TestUtils.ts';
4+
export * from './QueuePoller.ts';

pnpm-lock.yaml

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)