|
1 | 1 | import * as fs from 'fs' |
2 | 2 | import { nanoid } from 'nanoid' |
3 | 3 | import { sign } from 'jsonwebtoken' |
4 | | -import { GenericContainer, Network, StartedTestContainer, Wait } from 'testcontainers' |
5 | | -import { ExecResult } from 'testcontainers/dist/docker/types' |
| 4 | +import { |
| 5 | + GenericContainer, |
| 6 | + Network, |
| 7 | + StartedNetwork, |
| 8 | + StartedTestContainer, |
| 9 | + Wait, |
| 10 | +} from 'testcontainers' |
6 | 11 | import { attach, log } from '../utils/jest-custom-reporter' |
7 | 12 |
|
8 | | -/** |
9 | | - * A Relay contains a running relay container that has a unique ID and two promises: |
10 | | - * one for the `deno cache` and one for the `deno run` the required function |
11 | | - */ |
12 | 13 | export class Relay { |
13 | 14 | container: StartedTestContainer |
| 15 | + network: StartedNetwork |
14 | 16 | id: string |
15 | | - execCache: Promise<ExecResult> |
16 | | - execRun: Promise<ExecResult> |
17 | | - constructor( |
18 | | - container: StartedTestContainer, |
19 | | - id: string, |
20 | | - execCache: Promise<ExecResult>, |
21 | | - execRun: Promise<ExecResult> |
22 | | - ) { |
| 17 | + constructor(container: StartedTestContainer, network: StartedNetwork, id: string) { |
23 | 18 | this.container = container |
| 19 | + this.network = network |
24 | 20 | this.id = id |
25 | | - this.execCache = execCache |
26 | | - this.execRun = execRun |
27 | 21 | } |
28 | 22 |
|
29 | | - /** |
30 | | - * Safely stops the relay container and cleans up exec processes |
31 | | - */ |
32 | 23 | async stop(): Promise<void> { |
33 | 24 | try { |
34 | | - // Try to stop the container - this will trigger cleanup of exec instances |
35 | 25 | await this.container.stop({ timeout: 5000 }) |
36 | | - } catch (error: any) { |
37 | | - // Ignore "no such exec" errors during cleanup - they're harmless |
38 | | - if (!error?.message?.includes('no such exec')) { |
39 | | - throw error |
40 | | - } |
41 | | - // Container is already stopped or exec instances are gone, which is fine |
| 26 | + } finally { |
| 27 | + await this.network.stop() |
42 | 28 | } |
43 | 29 | } |
44 | 30 | } |
@@ -86,56 +72,64 @@ export async function runRelay( |
86 | 72 |
|
87 | 73 | // start relay and function |
88 | 74 | log(`start relay ${slug + '-' + id}`) |
89 | | - const startedRelay = await relay.start() |
90 | | - const execCache = startedRelay.exec(['deno', 'cache', `/home/deno/${slug}/index.ts`]) |
91 | | - const execRun = startedRelay.exec([ |
92 | | - 'deno', |
93 | | - 'run', |
94 | | - '--allow-all', |
95 | | - '--watch', |
96 | | - '--unstable', |
97 | | - `/home/deno/${slug}/index.ts`, |
98 | | - ]) |
| 75 | + const startedRelay = await relay.start().catch(async (error) => { |
| 76 | + await network.stop().catch((stopError) => log(`failed to stop network ${id}: ${stopError}`)) |
| 77 | + throw error |
| 78 | + }) |
| 79 | + const started = new Relay(startedRelay, network, id) |
| 80 | + const functionPath = `/home/deno/${slug}/index.ts` |
| 81 | + try { |
| 82 | + for (const command of [ |
| 83 | + ['deno', 'cache', functionPath], |
| 84 | + [ |
| 85 | + 'sh', |
| 86 | + '-c', |
| 87 | + 'deno run --allow-all --watch --unstable "$1" > /tmp/function.log 2>&1 < /dev/null &', |
| 88 | + 'sh', |
| 89 | + functionPath, |
| 90 | + ], |
| 91 | + ]) { |
| 92 | + const { exitCode, output } = await startedRelay.exec(command) |
| 93 | + if (exitCode !== 0) { |
| 94 | + throw new Error(`Failed to start function ${slug}: ${output}`) |
| 95 | + } |
| 96 | + } |
99 | 97 |
|
100 | | - // wait till function is running |
101 | | - log(`check function is healthy: ${slug + '-' + id}`) |
102 | | - for (let ctr = 0; ctr < 60; ctr++) { |
103 | | - try { |
104 | | - const healthCheck = await fetch( |
105 | | - `http://localhost:${startedRelay.getMappedPort(8081)}/${slug}`, |
106 | | - { |
107 | | - method: 'POST', |
108 | | - headers: { |
109 | | - Authorization: `Bearer ${sign({ name: 'check' }, jwtSecret)}`, |
110 | | - }, |
| 98 | + // wait till function is running |
| 99 | + log(`check function is healthy: ${slug + '-' + id}`) |
| 100 | + for (let ctr = 0; ctr < 60; ctr++) { |
| 101 | + try { |
| 102 | + const healthCheck = await fetch( |
| 103 | + `http://localhost:${startedRelay.getMappedPort(8081)}/${slug}`, |
| 104 | + { |
| 105 | + method: 'POST', |
| 106 | + headers: { |
| 107 | + Authorization: `Bearer ${sign({ name: 'check' }, jwtSecret)}`, |
| 108 | + }, |
| 109 | + } |
| 110 | + ) |
| 111 | + if (healthCheck.ok) { |
| 112 | + log(`function started to serve: ${slug + '-' + id}`) |
| 113 | + // Add a small delay after health check passes to ensure full readiness |
| 114 | + await new Promise((resolve) => setTimeout(resolve, 1000)) |
| 115 | + return started |
111 | 116 | } |
112 | | - ) |
113 | | - if (healthCheck.ok || healthCheck.status === 101) { |
114 | | - log(`function started to serve: ${slug + '-' + id}`) |
115 | | - // Add a small delay after health check passes to ensure full readiness |
116 | | - await new Promise((resolve) => setTimeout(resolve, 1000)) |
117 | | - return new Relay(startedRelay, id, execCache, execRun) |
118 | | - } |
119 | | - } catch (error) { |
120 | | - // Native fetch throws an error when it encounters HTTP 101 (WebSocket upgrade) |
121 | | - // If we get a TypeError (which fetch throws for protocol errors like 101), |
122 | | - // we consider the function ready to serve |
123 | | - if (error instanceof TypeError) { |
124 | | - // This likely means we got a 101 response that native fetch couldn't handle |
125 | | - // The server is responding, so consider it ready |
126 | | - log(`function started to serve (detected via error): ${slug + '-' + id}`) |
127 | | - await new Promise((resolve) => setTimeout(resolve, 1000)) |
128 | | - return new Relay(startedRelay, id, execCache, execRun) |
| 117 | + } catch { |
| 118 | + // relay not reachable yet (connection refused/reset), retry |
129 | 119 | } |
130 | | - // For other errors (connection refused, etc.), continue retrying |
| 120 | + await new Promise((resolve) => setTimeout(resolve, 500)) |
131 | 121 | } |
132 | | - await new Promise((resolve) => setTimeout(resolve, 500)) |
133 | | - } |
134 | 122 |
|
135 | | - // if function hasn't started, stop container and throw |
136 | | - log(`function failed to start: ${slug + '-' + id}`) |
137 | | - startedRelay.stop() |
138 | | - throw new Error("function haven'start correctly") |
| 123 | + // if function hasn't started, throw with its log |
| 124 | + log(`function failed to start: ${slug + '-' + id}`) |
| 125 | + const { output } = await startedRelay.exec(['cat', '/tmp/function.log']) |
| 126 | + throw new Error(`function hasn't started correctly: ${output}`) |
| 127 | + } catch (error) { |
| 128 | + await started |
| 129 | + .stop() |
| 130 | + .catch((stopError) => log(`failed to stop relay ${slug + '-' + id}: ${stopError}`)) |
| 131 | + throw error |
| 132 | + } |
139 | 133 | } |
140 | 134 |
|
141 | 135 | /** |
|
0 commit comments