+6
-1
.github/workflows/build-and-push-link-aws.yaml
+6
-1
.github/workflows/build-and-push-link-aws.yaml
···
1
1
name: build-and-push-link-aws
2
2
on:
3
3
workflow_dispatch:
4
+
pull_request:
5
+
paths:
6
+
- 'bskylink/**'
7
+
- 'Dockerfile.bskylink'
8
+
- '.github/workflows/build-and-push-link-aws.yaml'
4
9
5
10
env:
6
11
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
···
45
50
uses: docker/build-push-action@v4
46
51
with:
47
52
context: .
48
-
push: ${{ github.event_name != 'pull_request' }}
53
+
push: true
49
54
file: ./Dockerfile.bskylink
50
55
tags: ${{ steps.meta.outputs.tags }}
51
56
labels: ${{ steps.meta.outputs.labels }}
+55
-22
bskylink/src/bin.ts
+55
-22
bskylink/src/bin.ts
···
1
1
import {Database, envToCfg, httpLogger, LinkService, readEnv} from './index.js'
2
+
2
3
async function main() {
3
-
const env = readEnv()
4
-
const cfg = envToCfg(env)
5
-
if (cfg.db.migrationUrl) {
6
-
const migrateDb = Database.postgres({
7
-
url: cfg.db.migrationUrl,
8
-
schema: cfg.db.schema,
9
-
})
10
-
await migrateDb.migrateToLatestOrThrow()
11
-
await migrateDb.close()
12
-
}
4
+
try {
5
+
httpLogger.info('Starting blink service')
6
+
7
+
const env = readEnv()
8
+
const cfg = envToCfg(env)
9
+
10
+
httpLogger.info(
11
+
{
12
+
port: cfg.service.port,
13
+
safelinkEnabled: cfg.service.safelinkEnabled,
14
+
hasDbUrl: !!cfg.db.url,
15
+
hasDbMigrationUrl: !!cfg.db.migrationUrl,
16
+
},
17
+
'Configuration loaded',
18
+
)
19
+
20
+
if (cfg.db.migrationUrl) {
21
+
httpLogger.info('Running database migrations')
22
+
const migrateDb = Database.postgres({
23
+
url: cfg.db.migrationUrl,
24
+
schema: cfg.db.schema,
25
+
})
26
+
await migrateDb.migrateToLatestOrThrow()
27
+
await migrateDb.close()
28
+
httpLogger.info('Database migrations completed')
29
+
}
30
+
31
+
httpLogger.info('Creating LinkService')
32
+
const link = await LinkService.create(cfg)
33
+
34
+
if (link.ctx.cfg.service.safelinkEnabled) {
35
+
httpLogger.info('Starting Safelink client')
36
+
link.ctx.safelinkClient.runFetchEvents()
37
+
}
13
38
14
-
const link = await LinkService.create(cfg)
39
+
await link.start()
40
+
httpLogger.info('Link service is running')
15
41
16
-
if (link.ctx.cfg.service.safelinkEnabled) {
17
-
link.ctx.safelinkClient.runFetchEvents()
42
+
process.on('SIGTERM', async () => {
43
+
httpLogger.info('Link service is stopping')
44
+
await link.destroy()
45
+
httpLogger.info('Link service is stopped')
46
+
})
47
+
} catch (error) {
48
+
httpLogger.error(
49
+
{
50
+
error: String(error),
51
+
stack: error instanceof Error ? error.stack : undefined,
52
+
},
53
+
'Failed to start blink service',
54
+
)
55
+
process.exit(1)
18
56
}
19
-
20
-
await link.start()
21
-
httpLogger.info('link service is running')
22
-
process.on('SIGTERM', async () => {
23
-
httpLogger.info('link service is stopping')
24
-
await link.destroy()
25
-
httpLogger.info('link service is stopped')
26
-
})
27
57
}
28
58
29
-
main()
59
+
main().catch(error => {
60
+
console.error('Unhandled startup error:', error)
61
+
process.exit(1)
62
+
})
+10
bskylink/src/db/index.ts
+10
bskylink/src/db/index.ts
···
34
34
35
35
static postgres(opts: PgOptions): Database {
36
36
const {schema, url, txLockNonce} = opts
37
+
log.info(
38
+
{
39
+
schema,
40
+
poolSize: opts.poolSize,
41
+
poolMaxUses: opts.poolMaxUses,
42
+
poolIdleTimeoutMs: opts.poolIdleTimeoutMs,
43
+
},
44
+
'Creating database connection',
45
+
)
46
+
37
47
const pool =
38
48
opts.pool ??
39
49
new Pg.Pool({
+26
-4
bskyogcard/src/index.ts
+26
-4
bskyogcard/src/index.ts
···
62
62
// Start main application server
63
63
this.server = this.app.listen(this.ctx.cfg.service.port)
64
64
this.server.keepAliveTimeout = 90000
65
-
this.terminator = createHttpTerminator({server: this.server})
65
+
this.terminator = createHttpTerminator({
66
+
server: this.server,
67
+
gracefulTerminationTimeout: 15000, // 15s timeout for in-flight requests
68
+
})
66
69
await events.once(this.server, 'listening')
67
70
68
71
// Start separate metrics server
···
73
76
})
74
77
75
78
this.metricsServer = metricsApp.listen(this.ctx.cfg.service.metricsPort)
76
-
this.metricsTerminator = createHttpTerminator({server: this.metricsServer})
79
+
this.metricsTerminator = createHttpTerminator({
80
+
server: this.metricsServer,
81
+
gracefulTerminationTimeout: 2000, // 2s timeout for metrics server
82
+
})
77
83
await events.once(this.metricsServer, 'listening')
78
84
}
79
85
80
86
async destroy() {
87
+
const startTime = Date.now()
88
+
81
89
this.ctx.abortController.abort()
82
-
await this.terminator?.terminate()
83
-
await this.metricsTerminator?.terminate()
90
+
91
+
const shutdownPromises = []
92
+
93
+
if (this.terminator) {
94
+
shutdownPromises.push(this.terminator.terminate())
95
+
}
96
+
97
+
if (this.metricsTerminator) {
98
+
shutdownPromises.push(this.metricsTerminator.terminate())
99
+
}
100
+
101
+
await Promise.all(shutdownPromises)
102
+
103
+
const elapsed = Date.now() - startTime
104
+
const {httpLogger} = await import('./logger.js')
105
+
httpLogger.info(`Graceful shutdown completed in ${elapsed}ms`)
84
106
}
85
107
}