Skip to content

Commit db0c10f

Browse files
committed
connect sqs and log
1 parent 1ed1d60 commit db0c10f

File tree

9 files changed

+518
-3
lines changed

9 files changed

+518
-3
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
"semi": false
1717
},
1818
"dependencies": {
19+
"@dcl/schemas": "^7.4.1",
1920
"@well-known-components/env-config-provider": "^1.1.1",
2021
"@well-known-components/http-server": "^2.0.0",
2122
"@well-known-components/interfaces": "^1.4.1",
2223
"@well-known-components/logger": "^3.1.2",
23-
"@well-known-components/metrics": "^2.0.1-20220909150423.commit-8f7e5bc"
24+
"@well-known-components/metrics": "^2.0.1-20220909150423.commit-8f7e5bc",
25+
"@well-known-components/pushable-channel": "^1.0.3",
26+
"aws-sdk": "^2.1407.0"
2427
}
2528
}

src/adapters/runner.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { IBaseComponent } from "@well-known-components/interfaces";
2+
3+
export type RunnerComponentArg = {
4+
readonly isRunning: boolean
5+
}
6+
7+
export type IRunnerComponent = IBaseComponent & {
8+
runTask(delegate: (opt: RunnerComponentArg) => Promise<void>): void
9+
}
10+
11+
// this component runs a loop while the application is enabled. and waits until it
12+
// finishes the loop to stop the service
13+
export function createRunnerComponent(): IRunnerComponent {
14+
15+
let delegates: Promise<any>[] = []
16+
let isRunning = false
17+
18+
return {
19+
async start() {
20+
if (isRunning) throw new Error('Cannot run twice')
21+
isRunning = true
22+
},
23+
async stop() {
24+
isRunning = false
25+
await Promise.all(delegates)
26+
},
27+
runTask(delegate) {
28+
if (!isRunning) throw new Error('You can only run tasks while the component is running')
29+
delegates.push(delegate({ get isRunning() { return isRunning } }))
30+
},
31+
}
32+
}

src/adapters/sqs.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { IBaseComponent, IMetricsComponent } from '@well-known-components/interfaces'
2+
import { validateMetricsDeclaration } from '@well-known-components/metrics'
3+
import { AsyncQueue } from '@well-known-components/pushable-channel'
4+
import { SQS } from 'aws-sdk'
5+
import { AppComponents } from '../types'
6+
7+
export interface TaskQueueMessage {
8+
id: string
9+
}
10+
11+
export interface ITaskQueue<T> {
12+
// publishes a job for the queue
13+
publish(job: T): Promise<TaskQueueMessage>
14+
// awaits for a job. then calls and waits for the taskRunner argument.
15+
// the result is then returned to the wrapper function.
16+
consumeAndProcessJob<R>(taskRunner: (job: T, message: TaskQueueMessage) => Promise<R>): Promise<{ result: R | undefined }>
17+
}
18+
19+
export const queueMetrics = validateMetricsDeclaration({
20+
job_queue_duration_seconds: {
21+
type: IMetricsComponent.HistogramType,
22+
help: 'Duration of each job in seconds',
23+
labelNames: ["queue_name"],
24+
buckets: [1, 10, 100, 200, 300, 400, 500, 600, 700, 1000, 1200, 1600, 1800, 3600]
25+
},
26+
job_queue_enqueue_total: {
27+
type: IMetricsComponent.CounterType,
28+
help: "Total amount of enqueued jobs",
29+
labelNames: ["queue_name"],
30+
},
31+
job_queue_failures_total: {
32+
type: IMetricsComponent.CounterType,
33+
help: "Total amount of failed tasks",
34+
labelNames: ["queue_name"],
35+
},
36+
})
37+
38+
type SNSOverSQSMessage = {
39+
Message: string
40+
}
41+
42+
43+
export function createMemoryQueueAdapter<T>(components: Pick<AppComponents, "logs" | 'metrics'>, options: { queueName: string }): ITaskQueue<T> & IBaseComponent {
44+
type InternalElement = { message: TaskQueueMessage, job: T }
45+
const q = new AsyncQueue<InternalElement>((action) => void 0)
46+
let lastJobId = 0
47+
48+
const logger = components.logs.getLogger(options.queueName)
49+
50+
return {
51+
async stop() {
52+
q.close()
53+
},
54+
async publish(job) {
55+
const id = 'job-' + (++lastJobId).toString()
56+
const message: TaskQueueMessage = { id }
57+
q.enqueue({ job, message })
58+
logger.info(`Publishing job`, { id })
59+
components.metrics.increment('job_queue_enqueue_total', { queue_name: options.queueName })
60+
return message
61+
},
62+
async consumeAndProcessJob(taskRunner) {
63+
const it: InternalElement = (await q.next()).value
64+
if (it) {
65+
const { end } = components.metrics.startTimer('job_queue_duration_seconds', { queue_name: options.queueName })
66+
try {
67+
logger.info(`Processing job`, { id: it.message.id })
68+
const result = await taskRunner(it.job, it.message)
69+
logger.info(`Processed job`, { id: it.message.id })
70+
return { result, message: it.message }
71+
} catch (err: any) {
72+
components.metrics.increment('job_queue_failures_total', { queue_name: options.queueName })
73+
logger.error(err, { id: it.message.id })
74+
// q.enqueue(it)
75+
} finally {
76+
end()
77+
}
78+
}
79+
return { result: undefined }
80+
},
81+
}
82+
}
83+
84+
85+
export function createSqsAdapter<T>(components: Pick<AppComponents, "logs" | 'metrics'>, options: { queueUrl: string, queueRegion?: string }): ITaskQueue<T> {
86+
const logger = components.logs.getLogger(options.queueUrl)
87+
88+
const sqs = new SQS({ apiVersion: 'latest', region: options.queueRegion })
89+
90+
return {
91+
async publish(job) {
92+
const snsOverSqs: SNSOverSQSMessage = {
93+
Message: JSON.stringify(job)
94+
}
95+
96+
const published = await sqs.sendMessage(
97+
{
98+
QueueUrl: options.queueUrl,
99+
MessageBody: JSON.stringify(snsOverSqs),
100+
}).promise()
101+
102+
const m: TaskQueueMessage = { id: published.MessageId! }
103+
104+
logger.info(`Publishing job`, m as any)
105+
106+
components.metrics.increment('job_queue_enqueue_total', { queue_name: options.queueUrl })
107+
return m
108+
},
109+
async consumeAndProcessJob(taskRunner) {
110+
while (true) {
111+
const params: AWS.SQS.ReceiveMessageRequest = {
112+
AttributeNames: ['SentTimestamp'],
113+
MaxNumberOfMessages: 1,
114+
MessageAttributeNames: ['All'],
115+
QueueUrl: options.queueUrl,
116+
WaitTimeSeconds: 15,
117+
VisibilityTimeout: 3 * 3600 // 3 hours
118+
}
119+
120+
try {
121+
const response = await Promise.race([
122+
sqs.receiveMessage(params).promise(),
123+
timeout(30 * 60 * 1000, 'Timed out sqs.receiveMessage')
124+
])
125+
126+
if (response.Messages && response.Messages.length > 0) {
127+
for (const it of response.Messages) {
128+
const message: TaskQueueMessage = { id: it.MessageId! }
129+
const { end } = components.metrics.startTimer('job_queue_duration_seconds', { queue_name: options.queueUrl })
130+
try {
131+
const snsOverSqs: SNSOverSQSMessage = JSON.parse(it.Body!)
132+
logger.info(`Processing job`, { id: message.id, message: snsOverSqs.Message })
133+
const result = await taskRunner(JSON.parse(snsOverSqs.Message), message)
134+
logger.info(`Processed job`, { id: message.id })
135+
return { result, message }
136+
} catch (err: any) {
137+
logger.error(err)
138+
139+
components.metrics.increment('job_queue_failures_total', { queue_name: options.queueUrl })
140+
141+
return { result: undefined, message }
142+
} finally {
143+
await sqs.deleteMessage({ QueueUrl: options.queueUrl, ReceiptHandle: it.ReceiptHandle! }).promise()
144+
end()
145+
}
146+
}
147+
}
148+
logger.info(`No new messages in queue. Retrying for 15 seconds`)
149+
} catch (err: any) {
150+
logger.error(err)
151+
await sleep(1000)
152+
}
153+
}
154+
},
155+
}
156+
}
157+
158+
export async function sleep(ms: number) {
159+
return new Promise<void>((ok) => setTimeout(ok, ms))
160+
}
161+
162+
export async function timeout(ms: number, message: string) {
163+
return new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), ms))
164+
}

src/components.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,46 @@ import { createFetchComponent } from "./adapters/fetch"
55
import { createMetricsComponent, instrumentHttpServerWithMetrics } from "@well-known-components/metrics"
66
import { AppComponents, GlobalContext } from "./types"
77
import { metricDeclarations } from "./metrics"
8+
import { createMemoryQueueAdapter, createSqsAdapter } from "./adapters/sqs"
9+
import { DeploymentToSqs } from "@dcl/schemas/dist/misc/deployments-to-sqs"
10+
import AWS from "aws-sdk"
11+
import { createRunnerComponent } from "./adapters/runner"
12+
import mitt from "mitt"
813

914
// Initialize all the components of the app
1015
export async function initComponents(): Promise<AppComponents> {
1116
const config = await createDotEnvConfigComponent({ path: [".env.default", ".env"] })
1217
const metrics = await createMetricsComponent(metricDeclarations, { config })
1318
const logs = await createLogComponent({ metrics })
14-
const server = await createServerComponent<GlobalContext>({ config, logs }, {})
19+
const server = await createServerComponent<GlobalContext>({ config, logs }, {
20+
cors: {}
21+
})
1522
const statusChecks = await createStatusCheckComponent({ server, config })
1623
const fetch = await createFetchComponent()
1724

1825
await instrumentHttpServerWithMetrics({ metrics, server, config })
1926

27+
const AWS_REGION = await config.getString('AWS_REGION')
28+
if (AWS_REGION) {
29+
AWS.config.update({ region: AWS_REGION })
30+
}
31+
32+
const sqsQueue = await config.getString('TASK_QUEUE')
33+
const taskQueue = sqsQueue ?
34+
createSqsAdapter<DeploymentToSqs>({ logs, metrics }, { queueUrl: sqsQueue, queueRegion: AWS_REGION }) :
35+
createMemoryQueueAdapter<DeploymentToSqs>({ logs, metrics }, { queueName: "ConversionTaskQueue" })
36+
37+
const runner = createRunnerComponent()
38+
2039
return {
2140
config,
2241
logs,
2342
server,
2443
statusChecks,
2544
fetch,
2645
metrics,
46+
taskQueue,
47+
runner,
48+
deploymentsByPointer: mitt()
2749
}
2850
}

src/metrics.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { IMetricsComponent } from "@well-known-components/interfaces"
22
import { getDefaultHttpMetrics, validateMetricsDeclaration } from "@well-known-components/metrics"
33
import { metricDeclarations as logsMetricsDeclarations } from "@well-known-components/logger"
4+
import { queueMetrics } from "./adapters/sqs"
45

56
export const metricDeclarations = {
67
...getDefaultHttpMetrics(),
78
...logsMetricsDeclarations,
9+
...queueMetrics,
810
test_ping_counter: {
911
help: "Count calls to ping",
1012
type: IMetricsComponent.CounterType,

src/service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,14 @@ export async function main(program: Lifecycle.EntryPointParameters<AppComponents
2020

2121
// start ports: db, listeners, synchronizations, etc
2222
await startComponents()
23+
24+
const logger = components.logs.getLogger('main-loop')
25+
26+
components.runner.runTask(async (opt) => {
27+
while (opt.isRunning) {
28+
await components.taskQueue.consumeAndProcessJob(async (job, message) => {
29+
logger.log("Deployment " + JSON.stringify(job))
30+
})
31+
}
32+
})
2333
}

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type {
77
IMetricsComponent,
88
} from "@well-known-components/interfaces"
99
import { metricDeclarations } from "./metrics"
10+
import { ITaskQueue } from "./adapters/sqs"
11+
import { DeploymentToSqs } from "@dcl/schemas/dist/misc/deployments-to-sqs"
12+
import { IRunnerComponent } from "./adapters/runner"
13+
import { Emitter } from "mitt"
1014

1115
export type GlobalContext = {
1216
components: BaseComponents
@@ -19,6 +23,9 @@ export type BaseComponents = {
1923
server: IHttpServerComponent<GlobalContext>
2024
fetch: IFetchComponent
2125
metrics: IMetricsComponent<keyof typeof metricDeclarations>
26+
taskQueue: ITaskQueue<DeploymentToSqs>
27+
runner: IRunnerComponent
28+
deploymentsByPointer: Emitter<Record<string /* pointer */, DeploymentToSqs>>
2229
}
2330

2431
// components used in runtime

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"noImplicitReturns": true,
1313
"noImplicitAny": true,
1414
"noImplicitThis": true,
15+
"esModuleInterop": true,
1516
"forceConsistentCasingInFileNames": true
1617
},
1718
"include": [

0 commit comments

Comments
 (0)