Skip to content

Health Check Improvements #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
403bb5a
wip: configurable probes
stevensJourney May 5, 2025
c357a1e
use CoreModule in service
stevensJourney May 5, 2025
1ac789e
fix docker build
stevensJourney May 5, 2025
bfd8ed8
Add JSON Schema
stevensJourney May 6, 2025
d1414f6
cleanup schema and yaml templates
stevensJourney May 6, 2025
3c1c493
Add comments to PowerSyncConfig
stevensJourney May 6, 2025
f55ae73
comment
stevensJourney May 6, 2025
043f9df
fix docker build
stevensJourney May 6, 2025
b4fcb37
add probe unit tests
stevensJourney May 6, 2025
0dcae0d
cleanup router code and naming
stevensJourney May 7, 2025
ae3b79c
improve API for YAML custom tags
stevensJourney May 7, 2025
d747df4
fix typo
stevensJourney May 7, 2025
443dbfe
revert schema changes
stevensJourney May 7, 2025
6edfc13
Merge remote-tracking branch 'origin/main' into probes
stevensJourney May 7, 2025
5995812
fix optionality for healthchecks
stevensJourney May 7, 2025
21c1dc2
add missing changeset
stevensJourney May 7, 2025
3549f20
ping liveness probe for API mode
stevensJourney May 7, 2025
2aec7ee
fix boot log
stevensJourney May 7, 2025
eb5e797
update Open edition tag
stevensJourney May 8, 2025
d56592d
assign timer referrence
stevensJourney May 8, 2025
1c2f908
Merge remote-tracking branch 'origin/main' into probes
stevensJourney May 8, 2025
fca98b5
Merge branch 'main' into probes
stevensJourney May 8, 2025
eea14d0
Merge branch 'main' into probes
stevensJourney May 8, 2025
d04792c
Merge remote-tracking branch 'origin/main' into probes
stevensJourney May 8, 2025
6791d8b
update router engine notice
stevensJourney May 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/bright-gifts-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@powersync/service-core': minor
---

- Added `ServiceContextMode` to `ServiceContext`. This conveys the mode in which the PowerSync service was started in.
- `RouterEngine` is now always present on `ServiceContext`. The router will only configure actual servers, when started, if routes have been registered.
- Added typecasting to `!env` YAML custom tag function. YAML config environment variable substitution now supports casting string environment variables to `number` and `boolean` types.

```yaml
replication:
connections: []

storage:
type: mongodb

api:
parameters:
max_buckets_per_connection: !env PS_MAX_BUCKETS::number

healthcheck:
probes:
use_http: !env PS_MONGO_HEALTHCHECK::boolean
```
32 changes: 32 additions & 0 deletions .changeset/khaki-cows-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@powersync/service-image': minor
---

- Added typecasting to `!env` YAML custom tag function. YAML config environment variable substitution now supports casting string environment variables to `number` and `boolean` types.

```yaml
replication:
connections: []

storage:
type: mongodb

api:
parameters:
max_buckets_per_connection: !env PS_MAX_BUCKETS::number

healthcheck:
probes:
use_http: !env PS_MONGO_HEALTHCHECK::boolean
```

- Added the ability to customize healthcheck probe exposure in the configuration. Backwards compatibility is maintained if no `healthcheck->probes` config is provided.

```yaml
healthcheck:
probes:
# Health status can be accessed by reading files (previously always enabled)
use_filesystem: true
# Health status can be accessed via HTTP requests (previously enabled for API and UNIFIED service modes)
use_http: true
```
5 changes: 5 additions & 0 deletions .changeset/little-pianos-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/service-types': minor
---

Added healthcheck types to PowerSyncConfig
5 changes: 5 additions & 0 deletions .changeset/shaggy-bikes-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/lib-services-framework': minor
---

Switched default health check probe mechanism from filesystem to in-memory implementation. Consumers now need to manually opt-in to filesystem probes.
5 changes: 5 additions & 0 deletions .changeset/thick-wolves-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/service-module-core': minor
---

Initial core module release. This moves RouterEngine API route registrations, health check probe configuration and metrics configuration from the service runners to this shared module.
11 changes: 8 additions & 3 deletions libs/lib-services/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { ServiceAssertionError } from '@powersync/service-errors';
import _ from 'lodash';
import { ErrorReporter } from './alerts/definitions.js';
import { NoOpReporter } from './alerts/no-op-reporter.js';
import { MigrationManager } from './migrations/MigrationManager.js';
import { ProbeModule, TerminationHandler, createFSProbe, createTerminationHandler } from './signals/signals-index.js';
import { ServiceAssertionError } from '@powersync/service-errors';
import {
ProbeModule,
TerminationHandler,
createInMemoryProbe,
createTerminationHandler
} from './signals/signals-index.js';

export enum ContainerImplementation {
REPORTER = 'reporter',
Expand Down Expand Up @@ -45,7 +50,7 @@ export type ServiceIdentifier<T = unknown> = string | symbol | Newable<T> | Abst

const DEFAULT_GENERATORS: ContainerImplementationDefaultGenerators = {
[ContainerImplementation.REPORTER]: () => NoOpReporter,
[ContainerImplementation.PROBES]: () => createFSProbe(),
[ContainerImplementation.PROBES]: () => createInMemoryProbe(),
[ContainerImplementation.TERMINATION_HANDLER]: () => createTerminationHandler(),
[ContainerImplementation.MIGRATION_MANAGER]: () => new MigrationManager()
};
Expand Down
1 change: 1 addition & 0 deletions modules/module-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @powersync/service-module-core
67 changes: 67 additions & 0 deletions modules/module-core/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Functional Source License, Version 1.1, Apache 2.0 Future License

## Abbreviation

FSL-1.1-Apache-2.0

## Notice

Copyright 2023-2024 Journey Mobile, Inc.

## Terms and Conditions

### Licensor ("We")

The party offering the Software under these Terms and Conditions.

### The Software

The "Software" is each version of the software that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software.

### License Grant

Subject to your compliance with this License Grant and the Patents, Redistribution and Trademark clauses below, we hereby grant you the right to use, copy, modify, create derivative works, publicly perform, publicly display and redistribute the Software for any Permitted Purpose identified below.

### Permitted Purpose

A Permitted Purpose is any purpose other than a Competing Use. A Competing Use means making the Software available to others in a commercial product or service that:

1. substitutes for the Software;
2. substitutes for any other product or service we offer using the Software that exists as of the date we make the Software available; or
3. offers the same or substantially similar functionality as the Software.

Permitted Purposes specifically include using the Software:

1. for your internal use and access;
2. for non-commercial education;
3. for non-commercial research; and
4. in connection with professional services that you provide to a licensee using the Software in accordance with these Terms and Conditions.

### Patents

To the extent your use for a Permitted Purpose would necessarily infringe our patents, the license grant above includes a license under our patents. If you make a claim against any party that the Software infringes or contributes to the infringement of any patent, then your patent license to the Software ends immediately.

### Redistribution

The Terms and Conditions apply to all copies, modifications and derivatives of the Software.
If you redistribute any copies, modifications or derivatives of the Software, you must include a copy of or a link to these Terms and Conditions and not remove any copyright notices provided in or with the Software.

### Disclaimer

THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.

### Trademarks

Except for displaying the License Details and identifying us as the origin of the Software, you have no right under these Terms and Conditions to use our trademarks, trade names, service marks or product names.

## Grant of Future License

We hereby irrevocably grant you an additional license to use the Software under the Apache License, Version 2.0 that is effective on the second anniversary of the date we make the Software available. On or after that date, you may use the Software under the Apache License, Version 2.0, in which case the following will apply:

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
3 changes: 3 additions & 0 deletions modules/module-core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# PowerSync Service Module Core

Module which registers and configures basic core functionality for PowerSync services.
39 changes: 39 additions & 0 deletions modules/module-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@powersync/service-module-core",
"repository": "https://github.com/powersync-ja/powersync-service",
"types": "dist/index.d.ts",
"version": "0.0.0",
"main": "dist/index.js",
"license": "FSL-1.1-Apache-2.0",
"type": "module",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -b",
"build:tests": "tsc -b test/tsconfig.json",
"clean": "rm -rf ./dist && tsc -b --clean",
"test": "vitest"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./types": {
"import": "./dist/types/types.js",
"require": "./dist/types/types.js",
"default": "./dist/types/types.js"
}
},
"dependencies": {
"@powersync/lib-services-framework": "workspace:*",
"@powersync/service-core": "workspace:*",
"@powersync/service-rsocket-router": "workspace:*",
"@powersync/service-types": "workspace:*",
"fastify": "4.23.2",
"@fastify/cors": "8.4.1"
},
"devDependencies": {}
}
178 changes: 178 additions & 0 deletions modules/module-core/src/CoreModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import cors from '@fastify/cors';
import * as framework from '@powersync/lib-services-framework';
import * as core from '@powersync/service-core';
import { ReactiveSocketRouter } from '@powersync/service-rsocket-router';
import fastify from 'fastify';

export class CoreModule extends core.modules.AbstractModule {
constructor() {
super({
name: 'Core'
});
}

public async initialize(context: core.ServiceContextContainer): Promise<void> {
this.configureTags(context);

if ([core.system.ServiceContextMode.API, core.system.ServiceContextMode.UNIFIED].includes(context.serviceMode)) {
// Service should include API routes
this.registerAPIRoutes(context);
}

// Configures a Fastify server and RSocket server
this.configureRouterImplementation(context);

// Configures health check probes based off configuration
this.configureHealthChecks(context);

await this.configureMetrics(context);
}

protected configureTags(context: core.ServiceContextContainer) {
core.utils.setTags(context.configuration.metadata);
}

protected registerAPIRoutes(context: core.ServiceContextContainer) {
context.routerEngine.registerRoutes({
api_routes: [
...core.routes.endpoints.ADMIN_ROUTES,
...core.routes.endpoints.CHECKPOINT_ROUTES,
...core.routes.endpoints.SYNC_RULES_ROUTES
],
stream_routes: [...core.routes.endpoints.SYNC_STREAM_ROUTES],
socket_routes: [core.routes.endpoints.syncStreamReactive]
});
}

/**
* Configures the HTTP server which will handle routes once the router engine is started
*/
protected configureRouterImplementation(context: core.ServiceContextContainer) {
context.lifeCycleEngine.withLifecycle(context.routerEngine, {
start: async (routerEngine) => {
// The router engine will only start servers if routes have been registered
await routerEngine.start(async (routes) => {
const server = fastify.fastify();

server.register(cors, {
origin: '*',
allowedHeaders: ['Content-Type', 'Authorization', 'User-Agent', 'X-User-Agent'],
exposedHeaders: ['Content-Type'],
// Cache time for preflight response
maxAge: 3600
});

core.routes.configureFastifyServer(server, {
service_context: context,
routes: {
api: { routes: routes.api_routes },
sync_stream: {
routes: routes.stream_routes,
queue_options: {
concurrency: context.configuration.api_parameters.max_concurrent_connections,
max_queue_depth: 0
}
}
}
});

const socketRouter = new ReactiveSocketRouter<core.routes.Context>({
max_concurrent_connections: context.configuration.api_parameters.max_concurrent_connections
});

core.routes.configureRSocket(socketRouter, {
server: server.server,
service_context: context,
route_generators: routes.socket_routes
});

const { port } = context.configuration;

await server.listen({
host: '0.0.0.0',
port
});

framework.logger.info(`Running on port ${port}`);

return {
onShutdown: async () => {
framework.logger.info('Shutting down HTTP server...');
await server.close();
framework.logger.info('HTTP server stopped');
}
};
});
}
});
}

protected configureHealthChecks(context: core.ServiceContextContainer) {
const {
configuration: {
healthcheck: { probes }
}
} = context;

const exposesAPI = [core.system.ServiceContextMode.API, core.system.ServiceContextMode.UNIFIED].includes(
context.serviceMode
);

if (context.serviceMode == core.system.ServiceContextMode.API) {
/**
* In the pure API mode we don't currently have any other code which touches the probes.
* This configures a timer which will touch every 5 seconds.
*/
let timer: NodeJS.Timeout | null = null;
context.lifeCycleEngine.withLifecycle(null, {
start: () => {
timer = setInterval(() => {
context
.get<framework.ProbeModule>(framework.ContainerImplementation.PROBES)
.touch()
.catch((ex) => this.logger.error(`Caught error while updating liveness probe: ${ex}`));
}, 5_000);
},
stop: () => {
if (timer) {
clearInterval(timer);
timer = null;
}
}
});
}

/**
* Maintains backwards compatibility if LEGACY_DEFAULT is present by:
* - Enabling HTTP probes if the service started in API or UNIFIED mode
* - Always enabling filesystem probes always exposing HTTP probes
* Probe types must explicitly be selected if not using LEGACY_DEFAULT
*/
if (probes.use_http || (exposesAPI && probes.use_legacy)) {
context.routerEngine.registerRoutes({
api_routes: core.routes.endpoints.PROBES_ROUTES
});
}

if (probes.use_legacy || probes.use_filesystem) {
context.register(framework.ContainerImplementation.PROBES, framework.createFSProbe());
}
}

protected async configureMetrics(context: core.ServiceContextContainer) {
const apiMetrics = [core.metrics.MetricModes.API];
const streamMetrics = [core.metrics.MetricModes.REPLICATION, core.metrics.MetricModes.STORAGE];
const metricsModeMap: Partial<Record<core.system.ServiceContextMode, core.metrics.MetricModes[]>> = {
[core.system.ServiceContextMode.API]: apiMetrics,
[core.system.ServiceContextMode.SYNC]: streamMetrics,
[core.system.ServiceContextMode.UNIFIED]: [...apiMetrics, ...streamMetrics]
};

await core.metrics.registerMetrics({
service_context: context,
modes: metricsModeMap[context.serviceMode] ?? []
});
}

public async teardown(options: core.modules.TearDownOptions): Promise<void> {}
}
1 change: 1 addition & 0 deletions modules/module-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CoreModule.js';
1 change: 1 addition & 0 deletions modules/module-core/src/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// No types for this module yet
Loading