|
4 | 4 | * See License.AGPL.txt in the project root for license information.
|
5 | 5 | */
|
6 | 6 |
|
7 |
| -import { injectable, inject } from "inversify"; |
| 7 | +import { injectable } from "inversify"; |
8 | 8 | import express from "express";
|
9 |
| -import { TypeORM } from "@gitpod/gitpod-db/lib"; |
10 |
| -import { SpiceDBClientProvider } from "../authorization/spicedb"; |
11 | 9 | import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
|
12 |
| -import { v1 } from "@authzed/authzed-node"; |
13 |
| -import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; |
14 |
| -import { Redis } from "ioredis"; |
15 |
| -import { repeat } from "@gitpod/gitpod-protocol/lib/util/repeat"; |
16 |
| -import { Disposable, DisposableCollection } from "@gitpod/gitpod-protocol"; |
17 | 10 |
|
| 11 | +/** |
| 12 | + * ReadinessController is mimicking the behavior server had in the past: Behave as there is not ready probe - except during shutdown. |
| 13 | + * |
| 14 | + * Why? In Gitpod, our error strategy has always been "keep it local and retry", instead of "fail loud and have someone else handle it". |
| 15 | + * As we don't want to change this now, we keep the same behavior for most of the services lifetime. |
| 16 | + * |
| 17 | + * Only during shutdown, we want to signal that the service is not ready anymore, to reduce error peaks. |
| 18 | + */ |
18 | 19 | @injectable()
|
19 | 20 | export class ReadinessController {
|
20 |
| - @inject(TypeORM) protected readonly typeOrm: TypeORM; |
21 |
| - @inject(SpiceDBClientProvider) protected readonly spiceDBClientProvider: SpiceDBClientProvider; |
22 |
| - @inject(Redis) protected readonly redis: Redis; |
23 |
| - |
24 |
| - private readinessProbeEnabled: boolean = true; |
25 |
| - private disposables: DisposableCollection = new DisposableCollection(); |
| 21 | + private shutdown: boolean = false; |
26 | 22 |
|
27 | 23 | get apiRouter(): express.Router {
|
28 | 24 | const router = express.Router();
|
29 | 25 | this.addReadinessHandler(router);
|
30 | 26 | return router;
|
31 | 27 | }
|
32 | 28 |
|
33 |
| - public async start() { |
34 |
| - this.disposables.push(this.startPollingFeatureFlag()); |
35 |
| - } |
36 |
| - |
37 |
| - public async stop() { |
38 |
| - this.disposables.dispose(); |
| 29 | + public notifyShutdown(): void { |
| 30 | + this.shutdown = true; |
39 | 31 | }
|
40 | 32 |
|
41 | 33 | protected addReadinessHandler(router: express.Router) {
|
42 | 34 | router.get("/", async (_, res) => {
|
43 |
| - try { |
44 |
| - if (!this.readinessProbeEnabled) { |
45 |
| - log.debug("Readiness check skipped due to feature flag"); |
46 |
| - res.status(200); |
47 |
| - return; |
48 |
| - } |
49 |
| - |
50 |
| - // Check database connection |
51 |
| - const dbConnection = await this.checkDatabaseConnection(); |
52 |
| - if (!dbConnection) { |
53 |
| - log.warn("Readiness check failed: Database connection failed"); |
54 |
| - res.status(503).send("Database connection failed"); |
55 |
| - return; |
56 |
| - } |
57 |
| - |
58 |
| - // Check SpiceDB connection |
59 |
| - const spiceDBConnection = await this.checkSpiceDBConnection(); |
60 |
| - if (!spiceDBConnection) { |
61 |
| - log.warn("Readiness check failed: SpiceDB connection failed"); |
62 |
| - res.status(503).send("SpiceDB connection failed"); |
63 |
| - return; |
64 |
| - } |
65 |
| - |
66 |
| - // Check Redis connection |
67 |
| - const redisConnection = await this.checkRedisConnection(); |
68 |
| - if (!redisConnection) { |
69 |
| - log.warn("Readiness check failed: Redis connection failed"); |
70 |
| - res.status(503).send("Redis connection failed"); |
71 |
| - return; |
72 |
| - } |
73 |
| - |
74 |
| - // All connections are good |
75 |
| - res.status(200).send("Ready"); |
76 |
| - log.debug("Readiness check successful"); |
77 |
| - } catch (error) { |
78 |
| - log.error("Readiness check failed", error); |
79 |
| - res.status(503).send("Readiness check failed"); |
| 35 | + if (this.shutdown) { |
| 36 | + log.warn("Readiness check failed: Server is shutting down"); |
| 37 | + res.status(503).send("Server is shutting down"); |
| 38 | + return; |
80 | 39 | }
|
81 |
| - }); |
82 |
| - } |
83 | 40 |
|
84 |
| - private async checkDatabaseConnection(): Promise<boolean> { |
85 |
| - try { |
86 |
| - const connection = await this.typeOrm.getConnection(); |
87 |
| - // Simple query to verify connection is working |
88 |
| - await connection.query("SELECT 1"); |
89 |
| - log.debug("Database connection check successful"); |
90 |
| - return true; |
91 |
| - } catch (error) { |
92 |
| - log.error("Database connection check failed", error); |
93 |
| - return false; |
94 |
| - } |
95 |
| - } |
96 |
| - |
97 |
| - private async checkSpiceDBConnection(): Promise<boolean> { |
98 |
| - try { |
99 |
| - const client = this.spiceDBClientProvider.getClient(); |
100 |
| - |
101 |
| - // Send a request, to verify that the connection works |
102 |
| - const req = v1.ReadSchemaRequest.create({}); |
103 |
| - const response = await client.readSchema(req); |
104 |
| - log.debug("SpiceDB connection check successful", { schemaLength: response.schemaText.length }); |
105 |
| - |
106 |
| - return true; |
107 |
| - } catch (error) { |
108 |
| - log.error("SpiceDB connection check failed", error); |
109 |
| - return false; |
110 |
| - } |
111 |
| - } |
112 |
| - |
113 |
| - private async checkRedisConnection(): Promise<boolean> { |
114 |
| - try { |
115 |
| - // Simple PING command to verify connection is working |
116 |
| - const result = await this.redis.ping(); |
117 |
| - log.debug("Redis connection check successful", { result }); |
118 |
| - return result === "PONG"; |
119 |
| - } catch (error) { |
120 |
| - log.error("Redis connection check failed", error); |
121 |
| - return false; |
122 |
| - } |
123 |
| - } |
124 |
| - |
125 |
| - private startPollingFeatureFlag(): Disposable { |
126 |
| - return repeat(async () => { |
127 |
| - // Check feature flag first |
128 |
| - const readinessProbeEnabled = await getExperimentsClientForBackend().getValueAsync( |
129 |
| - "server_readiness_probe", |
130 |
| - true, // Default to readiness probe, skip if false |
131 |
| - {}, |
132 |
| - ); |
133 |
| - |
134 |
| - log.debug("Feature flag server_readiness_probe updated", { |
135 |
| - readinessProbeEnabled, |
136 |
| - oldValue: this.readinessProbeEnabled, |
137 |
| - }); |
138 |
| - this.readinessProbeEnabled = readinessProbeEnabled; |
139 |
| - }, 10_000); |
| 41 | + res.status(200).send("Ready"); |
| 42 | + log.debug("Readiness check successful"); |
| 43 | + }); |
140 | 44 | }
|
141 | 45 | }
|
0 commit comments