signalk-questdb 2.5.0 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/README.md +157 -1186
- package/dist/api-contract.d.ts +1 -1
- package/dist/config/schema.js +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/index.js +8 -45
- package/dist/index.js.map +1 -1
- package/dist/migration-checkpoint.d.ts +4 -4
- package/dist/migration-checkpoint.js +4 -4
- package/dist/restore.d.ts +10 -6
- package/dist/restore.js +22 -8
- package/dist/restore.js.map +1 -1
- package/doc/configuration.md +98 -0
- package/doc/deployment.md +249 -0
- package/doc/history-api.md +178 -0
- package/doc/import-export.md +179 -0
- package/doc/recording.md +187 -0
- package/doc/troubleshooting.md +243 -0
- package/doc/web-console.md +17 -0
- package/package.json +1 -1
- package/public/assets/{PluginConfigurationPanel-CAj6IDmY.js → PluginConfigurationPanel-DVQ7lQ2N.js} +2 -2
- package/public/remoteEntry.js +1 -1
package/dist/restore.js
CHANGED
|
@@ -201,6 +201,15 @@ function snapshotSQL(sinceIso, identitySinceIso, withKind) {
|
|
|
201
201
|
function isMissingKindColumn(err) {
|
|
202
202
|
return /Invalid column:\s*value_kind/i.test(err instanceof Error ? err.message : String(err));
|
|
203
203
|
}
|
|
204
|
+
// A leaf the server has written holds `value`, but a node can exist without
|
|
205
|
+
// one: meta published for a path creates it. A vessel name is a plain string
|
|
206
|
+
// at the context's root, with no leaf around it.
|
|
207
|
+
function inModel(node) {
|
|
208
|
+
if (typeof node === "object" && node !== null) {
|
|
209
|
+
return node.value !== undefined;
|
|
210
|
+
}
|
|
211
|
+
return node !== undefined;
|
|
212
|
+
}
|
|
204
213
|
function decodeValue(row) {
|
|
205
214
|
const text = row.valuetext;
|
|
206
215
|
if (text === null || text === undefined)
|
|
@@ -298,10 +307,11 @@ export async function restoreFromHistory(deps, options) {
|
|
|
298
307
|
// target that never expires.
|
|
299
308
|
if (!entry.byPath.has("navigation.position"))
|
|
300
309
|
continue;
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
// Stored "self" is a placeholder for whatever this server's self context
|
|
311
|
+
// is; every other context is already a real Signal K context string.
|
|
312
|
+
const context = storedContext === "self" ? selfContext : storedContext;
|
|
313
|
+
// Already drawn, from a fix at least as new as the stored one.
|
|
314
|
+
if (inModel(deps.getPath(`${context}.navigation.position`))) {
|
|
305
315
|
skippedLive++;
|
|
306
316
|
continue;
|
|
307
317
|
}
|
|
@@ -324,6 +334,13 @@ export async function restoreFromHistory(deps, options) {
|
|
|
324
334
|
entry.byPath.set(path, { ...newest, value, isName: false });
|
|
325
335
|
}
|
|
326
336
|
}
|
|
337
|
+
// Looked up after the leaves are joined, under the bare path the model
|
|
338
|
+
// keeps an object identity at.
|
|
339
|
+
for (const path of entry.byPath.keys()) {
|
|
340
|
+
if (inModel(deps.getPath(`${context}.${path}`))) {
|
|
341
|
+
entry.byPath.delete(path);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
327
344
|
// One update per distinct recorded time. These paths were sampled at
|
|
328
345
|
// different moments — an AIS name repeats every ~6 minutes while position
|
|
329
346
|
// arrives every few seconds — so putting them all under the newest
|
|
@@ -336,9 +353,6 @@ export async function restoreFromHistory(deps, options) {
|
|
|
336
353
|
: { path, value: row.value });
|
|
337
354
|
byTimestamp.set(row.ts, group);
|
|
338
355
|
}
|
|
339
|
-
// Stored "self" is a placeholder for whatever this server's self context
|
|
340
|
-
// is; every other context is already a real Signal K context string.
|
|
341
|
-
const context = storedContext === "self" ? selfContext : storedContext;
|
|
342
356
|
handleMessage({
|
|
343
357
|
context,
|
|
344
358
|
updates: [...byTimestamp].map(([timestamp, values]) => ({
|
|
@@ -354,7 +368,7 @@ export async function restoreFromHistory(deps, options) {
|
|
|
354
368
|
}
|
|
355
369
|
debug(`restored ${contexts} context(s), ${values} value(s) from history` +
|
|
356
370
|
(skippedStale ? `, skipped ${skippedStale} stale row(s)` : "") +
|
|
357
|
-
(skippedLive ? `, skipped ${skippedLive} already
|
|
371
|
+
(skippedLive ? `, skipped ${skippedLive} already in the model` : ""));
|
|
358
372
|
return { contexts, values, skippedStale, skippedLive };
|
|
359
373
|
}
|
|
360
374
|
//# sourceMappingURL=restore.js.map
|
package/dist/restore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"restore.js","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"restore.js","sourceRoot":"","sources":["../src/restore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
The config panel groups the settings as: the **status card** (running state,
|
|
4
|
+
rows, active paths, restore outcome, WAL state), the **update check** and
|
|
5
|
+
**image version** picker (latest, pre-releases and the last three stable
|
|
6
|
+
releases), **Connection**, **Recording**, **History**, and the collapsible
|
|
7
|
+
**Path filtering**, **Compression (on-disk)**, **InfluxDB Migration**, **Data
|
|
8
|
+
Export** and **Danger zone** sections. The Danger zone removes the container and
|
|
9
|
+
all of its data — including what Signal K's plugin uninstall cannot reach on
|
|
10
|
+
rootless Podman.
|
|
11
|
+
|
|
12
|
+
| Setting | Default | Description |
|
|
13
|
+
| ----------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
14
|
+
| QuestDB image version | `latest` | Image tag; the dropdown lists the latest release, pre-releases, and the last three stable releases |
|
|
15
|
+
| Manage QuestDB container | `true` | Let signalk-container run QuestDB; off = connect to an external QuestDB |
|
|
16
|
+
| QuestDB host | `127.0.0.1` | External mode only; in managed mode the address is resolved automatically |
|
|
17
|
+
| HTTP port | `9000` | External mode, or the host binding when "Bind to 0.0.0.0" is on; otherwise signalk-container allocates it |
|
|
18
|
+
| ILP port | `9009` | External mode, or the host binding when "Bind to 0.0.0.0" is on; otherwise signalk-container allocates it |
|
|
19
|
+
| PostgreSQL port | `8812` | Host binding for Grafana/psql when "Bind to 0.0.0.0" is on |
|
|
20
|
+
| QuestDB memory limit | `768m` | Hard cgroup cap on the container (empty = unlimited); the JVM heap gets half of it, so heap plus off-heap stays inside |
|
|
21
|
+
| QuestDB CPU limit (cores) | `1.5` | Fractional cores (0 = unlimited) |
|
|
22
|
+
| Default sampling rate (ms) | `2000` | Minimum ms between writes for any one path (0 = every update) |
|
|
23
|
+
| Per-path sampling rates (ms) | _(empty)_ | Glob → ms overrides, one `pattern ms` per line in the panel (0 = wire speed, see [Sampling](recording.md#what-is-recorded)) |
|
|
24
|
+
| Unchanged-value heartbeat (s) | `300` | A value equal to the last one recorded is written again only this often; changes are written at the sampling rate (0 = every sample, see [Change-only recording](recording.md#what-is-recorded)) |
|
|
25
|
+
| Write batch interval (ms) | `5000` | How often buffered samples are committed — one WAL transaction per table per commit (see [Performance](deployment.md#performance-pi-and-other-low-power-hosts)) |
|
|
26
|
+
| Record own vessel | `true` | Record the `self` context |
|
|
27
|
+
| Record AIS targets | `true` | Record other vessels |
|
|
28
|
+
| Record all sources | `false` | Record every source of a path, not only the one the source priorities prefer (see [Source priorities](recording.md#what-is-recorded)) |
|
|
29
|
+
| Restore vessels on startup | `false` | Replay each vessel's last recorded position after a restart (see [Startup restore](recording.md#startup-restore)) |
|
|
30
|
+
| Restore max age (minutes) | `9` | Only replay values recorded within this window |
|
|
31
|
+
| Allow `sourcePolicy=all` | `false` | Let History API callers split a path into one column per recording source (see [sourcePolicy=all](history-api.md#sourcepolicyall)) |
|
|
32
|
+
| QuestDB console webapp | `true` | Serve QuestDB's console inside the Signal K admin UI, admin only (see [Web Console](web-console.md)) |
|
|
33
|
+
| Retention (days) | `0` | Drop daily partitions older than this (0 = keep forever) |
|
|
34
|
+
| Keep own track | `true` | Archive the own vessel's positions before retention drops them (see [Retention and the archive](recording.md#retention-and-the-archive)) |
|
|
35
|
+
| Archived own-vessel paths | _(empty)_ | Glob patterns of own-vessel numeric paths to archive the same way, e.g. `environment.depth.*` |
|
|
36
|
+
| Archive resolution (s) | `10` | Archived rows are thinned to the first sample per interval (0 = every row) |
|
|
37
|
+
| Filter mode | `exclude` | `exclude` the matching paths, or `include` only the matching paths |
|
|
38
|
+
| Path patterns | _(empty)_ | Glob patterns, one per line, e.g. `environment.wind.*`; empty records everything |
|
|
39
|
+
| Compression codec | `lz4` | On-disk WAL segments and Parquet exports: `none`, `lz4` (fast) or `zstd` (smaller) |
|
|
40
|
+
| Compression level | `3` | ZSTD level 1–22, used only with `zstd` |
|
|
41
|
+
| Container network | `sk-network` | Shared Podman/Docker network QuestDB joins in managed mode, so the companion signalk-grafana reaches it by container DNS; host-port publication is separate ("Bind to 0.0.0.0") |
|
|
42
|
+
| Bind to 0.0.0.0 | `false` | Publish QuestDB's ports on all interfaces (see [Connectivity](deployment.md#connectivity)) |
|
|
43
|
+
|
|
44
|
+
## Default history provider
|
|
45
|
+
|
|
46
|
+
Which registered provider answers `/signalk/v2/api/history/` is the operator's
|
|
47
|
+
choice: pick it once under **Data → Preferences → Default History Provider**
|
|
48
|
+
(server 2.31 or newer); the server persists it as `historyApi.defaultProvider`
|
|
49
|
+
in `settings.json`, and it survives restarts and plugin load order. If no
|
|
50
|
+
default is configured, the server uses whichever provider registers first.
|
|
51
|
+
Versions 2.0.0 and earlier asked the server to make QuestDB the default on every
|
|
52
|
+
start; that is gone as of 2.0.1 — it could silently override a default you had
|
|
53
|
+
chosen, and on servers with security enabled it never worked at all.
|
|
54
|
+
|
|
55
|
+
## QuestDB updates
|
|
56
|
+
|
|
57
|
+
The panel's **update check** compares the running QuestDB with its latest stable
|
|
58
|
+
release. **Update** pulls that release, recreates the container on it and saves
|
|
59
|
+
it as the image version.
|
|
60
|
+
|
|
61
|
+
After the plugin itself was updated, and at its very first start, the plugin
|
|
62
|
+
also checks once on its own, in the background. It does so only in managed mode,
|
|
63
|
+
and only where the image version is a tag that moves, such as the default
|
|
64
|
+
`latest`. If QuestDB runs an older release than the latest stable one, the
|
|
65
|
+
plugin pulls the tag again and recreates the container on it. The image version
|
|
66
|
+
stays `latest`. Without this check, QuestDB on `latest` would stay on whatever
|
|
67
|
+
release the first install pulled, because signalk-container pulls an image only
|
|
68
|
+
when it is missing. An image version that names one release, such as `9.4.3`,
|
|
69
|
+
stays as it is, and so does one that **Update** saved while the check was
|
|
70
|
+
waiting. QuestDB is down while it starts again, as with **Update**. Progress
|
|
71
|
+
goes to the debug log, and failures go to the server log. A recreate that fails
|
|
72
|
+
half way and leaves QuestDB stopped or without a container is followed by a
|
|
73
|
+
start, as the plugin's own start would do. Only if that fails too does the
|
|
74
|
+
status line say so.
|
|
75
|
+
|
|
76
|
+
With [signalk-internet](https://github.com/meri-imperiumi/signalk-internet)
|
|
77
|
+
enabled, the check waits for a free link. On a metered, captive or offline link
|
|
78
|
+
it asks again from time to time, for as long as the plugin runs. Without
|
|
79
|
+
signalk-internet, the link counts as free. Telling the two apart needs
|
|
80
|
+
signalk-container 1.38.0 or later; with an older one, the check does not run. A
|
|
81
|
+
failure to read the running or the latest version is also tried again later.
|
|
82
|
+
|
|
83
|
+
The check is done once QuestDB runs the latest release, or once its container
|
|
84
|
+
runs the image the pull brought, even if QuestDB still reports an older release
|
|
85
|
+
afterwards. It then waits for the next plugin update. The next start checks
|
|
86
|
+
again wherever the container keeps its old image, or where the new one cannot
|
|
87
|
+
be told from the old:
|
|
88
|
+
|
|
89
|
+
- the plugin stopped first;
|
|
90
|
+
- the download failed, or something else failed before the recreate;
|
|
91
|
+
- an InfluxDB import, a GPX import or an export was running;
|
|
92
|
+
- the string table was being rebuilt;
|
|
93
|
+
- the container was stopped, which the check leaves as it is;
|
|
94
|
+
- the container was wedged;
|
|
95
|
+
- the tag on Docker Hub did not name the latest release yet.
|
|
96
|
+
|
|
97
|
+
A tag that never reaches the latest release, such as `9` while the latest is
|
|
98
|
+
`10.1.0`, therefore costs a pull and a look at GitHub at every start.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- Node.js 24 or newer
|
|
6
|
+
- QuestDB 8.1 or newer (resolution queries use `SAMPLE BY … FROM … TO`). This
|
|
7
|
+
applies to an external QuestDB and to a pinned **QuestDB image version** in
|
|
8
|
+
managed mode; `latest` always qualifies
|
|
9
|
+
- Signal K server — 2.31 or newer to choose the default history provider in the
|
|
10
|
+
admin UI
|
|
11
|
+
- For managed mode: the
|
|
12
|
+
[signalk-container](https://github.com/dirkwa/signalk-container) plugin, and
|
|
13
|
+
Podman 5.4 or newer (what Debian 13 "trixie" ships) or Docker.
|
|
14
|
+
signalk-container 1.29.0 or newer is recommended; the plugin runs with 1.14.0
|
|
15
|
+
or newer, but older versions lose parts of it — below 1.19.0 the Danger zone
|
|
16
|
+
cannot delete data kept on a named volume, below 1.26 the macOS open-files
|
|
17
|
+
problem shows as a generic error, below 1.29.0 a container the runtime refuses
|
|
18
|
+
to recreate is not reported (see [Troubleshooting](troubleshooting.md)). On rootless Podman below
|
|
19
|
+
5.5 the plugin's open-files request is inherited from the podman service
|
|
20
|
+
rather than granted per container
|
|
21
|
+
([containers/podman#25881](https://github.com/containers/podman/issues/25881));
|
|
22
|
+
signalk-container accounts for this.
|
|
23
|
+
|
|
24
|
+
## Connectivity
|
|
25
|
+
|
|
26
|
+
In **managed mode** the plugin does not need `QuestDB host` to be correct for
|
|
27
|
+
your deployment — signalk-container resolves the right address automatically,
|
|
28
|
+
whether Signal K runs on bare metal or is itself containerized:
|
|
29
|
+
|
|
30
|
+
- **Bind to 0.0.0.0 = off (default).** QuestDB stays private. signalk-container
|
|
31
|
+
binds its ports to the host loopback (bare-metal Signal K) or attaches QuestDB
|
|
32
|
+
to Signal K's own container network (containerized Signal K), and the plugin
|
|
33
|
+
connects to whatever address it reports back. Nothing is exposed to the
|
|
34
|
+
network. QuestDB is also attached to the shared `Container network` so the
|
|
35
|
+
companion [signalk-grafana](https://github.com/dirkwa/signalk-grafana) plugin
|
|
36
|
+
still reaches it by container DNS. This is the recommended setup and fixes
|
|
37
|
+
connectivity for Signal K in a container.
|
|
38
|
+
- **Bind to 0.0.0.0 = on.** QuestDB stays attached to the shared
|
|
39
|
+
`Container network`; the setting publishes its HTTP/ILP/PostgreSQL ports on
|
|
40
|
+
all host interfaces, using the configured port numbers. Enable this only to
|
|
41
|
+
reach QuestDB from another machine or from a Grafana running in a separate
|
|
42
|
+
Docker instance. When Signal K itself is containerized, the plugin probes
|
|
43
|
+
which address reaches the published ports — its own loopback first, then the
|
|
44
|
+
runtime's `host.containers.internal` gateway — and uses the one QuestDB
|
|
45
|
+
answers on.
|
|
46
|
+
|
|
47
|
+
In **external mode** (`Manage QuestDB container` off) the plugin connects to the
|
|
48
|
+
QuestDB you point it at via `QuestDB host` and the HTTP/ILP ports.
|
|
49
|
+
|
|
50
|
+
## Data storage
|
|
51
|
+
|
|
52
|
+
QuestDB's data root is the plugin's own data directory,
|
|
53
|
+
`<config>/plugin-config-data/signalk-questdb`, whatever the deployment. Data
|
|
54
|
+
survives container restarts, image upgrades, and plugin disable/enable cycles.
|
|
55
|
+
How it reaches the container differs:
|
|
56
|
+
|
|
57
|
+
- **Bare-metal Signal K**: that directory, mounted at `/var/lib/questdb`.
|
|
58
|
+
- **Signal K in a container with its config directory bind-mounted**: the exact
|
|
59
|
+
host path of that directory, mounted at `/var/lib/questdb`.
|
|
60
|
+
- **Signal K in a container with its config directory on a named volume**: the
|
|
61
|
+
volume cannot be mounted from a subdirectory, so it is mounted whole at
|
|
62
|
+
`/signalk-vols/<volume>` and QuestDB is pointed at the plugin's directory
|
|
63
|
+
inside it (`QUESTDB_DATA_DIR`). A database found at the volume's root — where
|
|
64
|
+
a whole-volume mount put it — is moved into the plugin's directory on start;
|
|
65
|
+
both are the same volume, so nothing is copied. A purge deletes the directory
|
|
66
|
+
from the Signal K process, since the runtime cannot mount the volume by Signal
|
|
67
|
+
K's path; where QuestDB's user owns it and the Signal K user cannot delete it,
|
|
68
|
+
the purge says so and the directory is deleted by hand.
|
|
69
|
+
|
|
70
|
+
QuestDB's entrypoint makes its data root its own on start, so the plugin keeps
|
|
71
|
+
the one file it writes alongside — the import checkpoint,
|
|
72
|
+
`signalk-questdb.influx-import-checkpoint.json` — next to the directory rather
|
|
73
|
+
than in it. A purge removes it, whether or not the data could be deleted.
|
|
74
|
+
|
|
75
|
+
## Durability on power loss
|
|
76
|
+
|
|
77
|
+
The managed container runs QuestDB with `cairo.commit.mode=sync`.
|
|
78
|
+
|
|
79
|
+
QuestDB's default is `nosync`: nothing on the ingest path is ever fsynced, and
|
|
80
|
+
durability is left to the OS page cache. On a power cut an arbitrary subset of
|
|
81
|
+
recent writes survives — including the case where a commit record reaches disk
|
|
82
|
+
while the row data it describes does not. QuestDB then fails while _opening_
|
|
83
|
+
that table's partition, which suspends the table permanently: neither a lossless
|
|
84
|
+
`RESUME WAL` nor a bounded `RESUME WAL FROM TXN` skip can get past it, because
|
|
85
|
+
the failure happens before any transaction is read. Repair means hand-patching
|
|
86
|
+
the commit record.
|
|
87
|
+
|
|
88
|
+
With `sync`, partition columns are fsynced before the commit record is written
|
|
89
|
+
and WAL segments are fsynced on commit, so the stored state can never claim data
|
|
90
|
+
that isn't durable. The cost is a few fsyncs per commit interval — writes are
|
|
91
|
+
already batched (see **Write batch interval**), so at boat data rates it is
|
|
92
|
+
negligible against losing a table.
|
|
93
|
+
|
|
94
|
+
**Running an external QuestDB?** Set this yourself in `server.conf`:
|
|
95
|
+
|
|
96
|
+
```ini
|
|
97
|
+
cairo.commit.mode=sync
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## WAL suspension
|
|
101
|
+
|
|
102
|
+
QuestDB ingestion is two-stage: the sequencer durably commits every batch (the
|
|
103
|
+
writer sees success), then a background job applies the transactions to the
|
|
104
|
+
partitions. When that job hits an error it **suspends the table** — the
|
|
105
|
+
sequencer keeps accepting rows, nothing is applied, queries serve stale data,
|
|
106
|
+
and no client-visible error is raised anywhere. Left alone, recording stalls
|
|
107
|
+
silently for as long as nobody looks.
|
|
108
|
+
|
|
109
|
+
The plugin watches for this. A suspended table of its own (in external mode,
|
|
110
|
+
only the three tables above and the two archive tables — never someone else's)
|
|
111
|
+
is logged loudly, raised as
|
|
112
|
+
the Signal K notification `notifications.signalk-questdb.walSuspended`, and
|
|
113
|
+
shown on the status card as **WAL suspended**. Two causes need two remedies:
|
|
114
|
+
|
|
115
|
+
- **Transient** — disk full since cleared, out of memory, file-descriptor
|
|
116
|
+
exhaustion: a plain `RESUME WAL` replays every pending transaction,
|
|
117
|
+
losslessly. The plugin does this by itself, once per stall point; the panel's
|
|
118
|
+
**Resume** button does the same on request, and the banner clears as the
|
|
119
|
+
backlog drains.
|
|
120
|
+
- **Unreadable segment** — an unclean container stop mid-write corrupts the
|
|
121
|
+
in-flight segment's files, and every apply attempt fails on them within
|
|
122
|
+
milliseconds, so a plain resume re-suspends at once, forever. The only way
|
|
123
|
+
forward is `RESUME WAL FROM TXN` past the unreadable data, which loses what it
|
|
124
|
+
skips. The plugin prepares that but never applies it on its own:
|
|
125
|
+
**/wal-diagnosis** scrapes the engine log for the real reason (the
|
|
126
|
+
`wal_tables()` error fields often carry none, and a container recreate wipes
|
|
127
|
+
the engine's memory of it) and computes the minimal skip — the first
|
|
128
|
+
transaction of the segment after the stuck one — with the number of
|
|
129
|
+
transactions and the wall-clock window of ingestion it would drop. The panel
|
|
130
|
+
shows those numbers and offers **Skip**; if the stuck segment is the newest
|
|
131
|
+
one, skipping drops the entire pending backlog, and the panel says so.
|
|
132
|
+
|
|
133
|
+
## Out of memory or not answering
|
|
134
|
+
|
|
135
|
+
QuestDB caps the native memory it tracks below the container's memory limit
|
|
136
|
+
and fails any allocation past the cap with `global RSS memory limit exceeded`.
|
|
137
|
+
When something holds memory at the cap, queries fail, the WAL apply job then
|
|
138
|
+
suspends the tables, and in the end QuestDB stops accepting HTTP connections. The JVM stays up throughout, so the
|
|
139
|
+
container's restart policy never acts.
|
|
140
|
+
|
|
141
|
+
In managed mode the plugin checks for this every minute while it runs, and
|
|
142
|
+
restarts the QuestDB container when:
|
|
143
|
+
|
|
144
|
+
- QuestDB recently reported its global memory limit, and over consecutive
|
|
145
|
+
checks its native memory is still held at that limit, it fails even the
|
|
146
|
+
health check with it, or it has stopped answering. A single large query
|
|
147
|
+
frees what it held when it ends, so it does not count, nor does a client that
|
|
148
|
+
keeps asking for one.
|
|
149
|
+
- the container is running but QuestDB has answered nothing for several
|
|
150
|
+
minutes, over consecutive checks. A QuestDB busy with a long statement still
|
|
151
|
+
answers it in the end, and no check counts while a parquet export streams
|
|
152
|
+
from it.
|
|
153
|
+
|
|
154
|
+
Before the restart the plugin writes QuestDB's message and its last error lines
|
|
155
|
+
to the server log, shows the reason on the status card, and raises the Signal K
|
|
156
|
+
notification `notifications.signalk-questdb.watchdog`. After the restart it
|
|
157
|
+
logs the outcome. Once QuestDB answers again the notification returns to normal;
|
|
158
|
+
when it answered right after the restart, the notification keeps the time and
|
|
159
|
+
the reason. Restarts are rate limited, spaced apart and capped per few hours of
|
|
160
|
+
the plugin running; past that the plugin logs that the restart is held back and
|
|
161
|
+
keeps the alert up. A restart that fails, because stopping or starting the
|
|
162
|
+
container failed, counts against none of these limits: the plugin tries again a
|
|
163
|
+
few minutes later, since the container may be left stopped. It reports the
|
|
164
|
+
first failure; while the retries keep failing, they go to the debug log only.
|
|
165
|
+
|
|
166
|
+
The plugin leaves a container alone if someone else stopped it, and starts one
|
|
167
|
+
that its own restart left stopped. One exception: a container stopped by hand
|
|
168
|
+
while a stop of the plugin's is still pending, or right after a restart of the
|
|
169
|
+
plugin's before the plugin has seen it running again, may be started again too.
|
|
170
|
+
While a stop of the plugin's hangs, it sends no further stop. An external
|
|
171
|
+
QuestDB is never restarted. A
|
|
172
|
+
QuestDB that is already stuck when the plugin starts, as after a Signal K
|
|
173
|
+
restart while it was stuck, is not covered: restarting the plugin stops the
|
|
174
|
+
container and starts it afresh.
|
|
175
|
+
|
|
176
|
+
The managed container runs with QuestDB's query caches off: history reads put
|
|
177
|
+
their time range into the SQL, so the caches never got a hit, and on older
|
|
178
|
+
QuestDB releases they held memory.
|
|
179
|
+
|
|
180
|
+
## Performance (Pi and other low-power hosts)
|
|
181
|
+
|
|
182
|
+
- **Default sampling rate** of 2000 ms limits each path to one write per two
|
|
183
|
+
seconds, keeping write volume modest on busy NMEA 2000 buses; per-path
|
|
184
|
+
overrides allow faster rates, or wire speed with 0, for critical paths while
|
|
185
|
+
slow-changing ones stay throttled.
|
|
186
|
+
- **Change-only recording** skips a value that has not changed since it was last
|
|
187
|
+
written until the heartbeat is due. Paths that hold steady — which is
|
|
188
|
+
most of them — cost one row per heartbeat instead of one per sampling
|
|
189
|
+
interval, which cuts both disk use and WAL apply work (see [Recording](recording.md#what-is-recorded)).
|
|
190
|
+
- **Resource caps** of 768 MB RAM and 1.5 CPU cores (cgroup limits via
|
|
191
|
+
signalk-container) keep QuestDB from squeezing co-resident containers like
|
|
192
|
+
Grafana, mayara, or signalk-backup. The JVM heap gets half of the memory
|
|
193
|
+
cap, so total footprint (heap plus off-heap) is bounded, and a JVM that runs
|
|
194
|
+
out of heap exits so the container restarts instead of hanging. Set the
|
|
195
|
+
memory limit to empty or the CPU limit to `0` to lift the cap on roomier
|
|
196
|
+
hosts.
|
|
197
|
+
- **Worker tuning** cuts idle CPU. QuestDB sizes its worker pools from the host
|
|
198
|
+
core count and busy-polls before sleeping — sensible on a server, wasteful on
|
|
199
|
+
a mostly idle boat database. The plugin pins its worker pools down and quiets
|
|
200
|
+
QuestDB's per-commit logging, keeping the messages that matter. One caveat
|
|
201
|
+
when upgrading the QuestDB image: an unrecognised setting is ignored silently,
|
|
202
|
+
so a property renamed between releases looks exactly like one that worked.
|
|
203
|
+
- **Batching.** Each commit is one WAL transaction per table, and with
|
|
204
|
+
deduplicated tables the apply cost of a transaction grows with partition size
|
|
205
|
+
— frequent tiny commits eventually outpace what a Pi can apply and recording
|
|
206
|
+
stalls. Bigger batches keep the WAL healthy; the cost is that at most one
|
|
207
|
+
interval of buffered samples is lost on a hard crash.
|
|
208
|
+
|
|
209
|
+
The console's Monitoring view flags any table whose 90th-percentile WAL
|
|
210
|
+
transaction stays under QuestDB's recommended batch size (100 rows) as **"Small
|
|
211
|
+
transactions — consider batching"**. `signalk_position` triggers this
|
|
212
|
+
structurally: it holds exactly one path, so its share of every commit is just
|
|
213
|
+
the handful of GPS fixes since the last one, while the other tables spread
|
|
214
|
+
hundreds of paths across each transaction. At vessel data rates the alert is
|
|
215
|
+
cosmetic; the numbers that matter are on the same page — **Write Amplification**
|
|
216
|
+
near 1x, **Pending Rows** 0 and **Transaction Lag** 0 mean the WAL apply is
|
|
217
|
+
healthy. Clearing the alert for the position table would need roughly 100 fixes
|
|
218
|
+
per commit, a batch window in the minutes; the trade-off is up to one interval
|
|
219
|
+
of buffered data lost on a hard crash, and history that is that much less live.
|
|
220
|
+
|
|
221
|
+
## Grafana
|
|
222
|
+
|
|
223
|
+
Connect Grafana to QuestDB through the PostgreSQL data source (user `admin`,
|
|
224
|
+
password `quest`, database `qdb`).
|
|
225
|
+
|
|
226
|
+
- The companion [signalk-grafana](https://github.com/dirkwa/signalk-grafana)
|
|
227
|
+
plugin wires this up for you: it runs Grafana as a managed container and
|
|
228
|
+
reaches QuestDB by container DNS on the shared `sk-network`, so no port needs
|
|
229
|
+
publishing.
|
|
230
|
+
- A Grafana **on the Signal K host** can reach the PostgreSQL port without
|
|
231
|
+
binding: with "Bind to 0.0.0.0" off, signalk-container binds it to loopback at
|
|
232
|
+
a host port of its choosing — `podman port <container>` shows which (the
|
|
233
|
+
container is `signalk-questdb` behind the `sk-` prefix). With "Bind to
|
|
234
|
+
0.0.0.0" on it is the configured port, 8812 by default.
|
|
235
|
+
- A Grafana in a **separate Docker instance or on another machine** needs "Bind
|
|
236
|
+
to 0.0.0.0" and the host's LAN address (`192.168.0.122:8812`, say). That
|
|
237
|
+
publishes QuestDB's ports to your entire network; only enable it behind a
|
|
238
|
+
firewall you trust.
|
|
239
|
+
|
|
240
|
+
Example query:
|
|
241
|
+
|
|
242
|
+
```sql
|
|
243
|
+
SELECT ts AS time, avg(value) AS sog
|
|
244
|
+
FROM signalk
|
|
245
|
+
WHERE path = 'navigation.speedOverGround'
|
|
246
|
+
AND context = 'self'
|
|
247
|
+
AND ts BETWEEN $__timeFrom() AND $__timeTo()
|
|
248
|
+
SAMPLE BY $__interval
|
|
249
|
+
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# History API
|
|
2
|
+
|
|
3
|
+
## v2 (REST — `/signalk/v2/api/history/`)
|
|
4
|
+
|
|
5
|
+
Registered via `app.registerHistoryApiProvider()`. Supports all aggregate
|
|
6
|
+
methods:
|
|
7
|
+
|
|
8
|
+
| Method | QuestDB mapping |
|
|
9
|
+
| -------------- | ------------------------------------------------------------------------------- |
|
|
10
|
+
| `average` | `avg(value)` |
|
|
11
|
+
| `min` | `min(value)` |
|
|
12
|
+
| `max` | `max(value)` |
|
|
13
|
+
| `first` | `first(value)` |
|
|
14
|
+
| `last` | `last(value)` |
|
|
15
|
+
| `mid` | `(min + max) / 2` |
|
|
16
|
+
| `middle_index` | the middle row of each bucket, by time — a recorded value, not a computed one |
|
|
17
|
+
| `sma` | the `average` bucket, then a moving window over N buckets (`sma:N`, default 5) |
|
|
18
|
+
| `ema` | the `average` bucket, then an exponential moving average (`ema:α`, default 0.2) |
|
|
19
|
+
|
|
20
|
+
A sample for `sma` and `ema` is one row of the series the window runs over: a
|
|
21
|
+
resolution bucket (its average) when the request names a resolution, a raw row
|
|
22
|
+
otherwise. So `sma:5` at `resolution=180` is a 15-minute moving average, on the
|
|
23
|
+
same grid as every other column in the response. An empty bucket keeps its place
|
|
24
|
+
in the window: the average there is of what the window still holds, and a value
|
|
25
|
+
leaves it N buckets after it arrived. `ema` carries its last value across an
|
|
26
|
+
empty bucket instead.
|
|
27
|
+
|
|
28
|
+
Buckets start at the request's `from` and cover the whole range, empty ones
|
|
29
|
+
included. A bucket with no row is not always missing data. Change-only recording (see
|
|
30
|
+
[Recording](recording.md#what-is-recorded)) writes an unchanged value only once per heartbeat, so an empty bucket
|
|
31
|
+
takes the last sample before it — in every aggregate, since every aggregate of a
|
|
32
|
+
steady value is that value — for up to twice the heartbeat after the sample was
|
|
33
|
+
taken, and never into a bucket that has not started yet. At the start of a
|
|
34
|
+
range the sample comes from just before it. Past that window, and with the
|
|
35
|
+
heartbeat set to 0, an empty bucket is `null`. This happens before `sma` and
|
|
36
|
+
`ema`, so a held value counts as a sample. `middle_index` and
|
|
37
|
+
`navigation.position` return recorded rows only.
|
|
38
|
+
|
|
39
|
+
A path recorded in radians — a heading, a course, a wind angle; `units: rad` in
|
|
40
|
+
the server's metadata for the path — is averaged as a vector: `average`, `sma`
|
|
41
|
+
and `ema` take the angle of the mean sine and cosine, so 359° and 1° average to
|
|
42
|
+
0°, not 180°. The result keeps the path's convention, `[0, 2π)` for a heading or
|
|
43
|
+
`(−π, π]` for an apparent wind angle; samples that cancel, 0° and 180° in equal
|
|
44
|
+
measure, have no mean direction and give `null`. `min`, `max` and `mid` stay
|
|
45
|
+
arithmetic.
|
|
46
|
+
|
|
47
|
+
`navigation.position` answers `first`, `last` and `middle_index` — each a point
|
|
48
|
+
the vessel was at. Any other method runs `first`, and the column's `method` says
|
|
49
|
+
so. A text path (rows in `signalk_str`) answers `first`, `last` and
|
|
50
|
+
`middle_index`. With a resolution, `average`, `min`, `max` and `mid` run `last`,
|
|
51
|
+
the state in force at the bucket's end, and the column's `method` says so.
|
|
52
|
+
`sma` and `ema` read the numeric table only, so a text path has no values for
|
|
53
|
+
them.
|
|
54
|
+
|
|
55
|
+
A method name outside the table above fails the request with an error naming
|
|
56
|
+
the path and the method.
|
|
57
|
+
|
|
58
|
+
Query example:
|
|
59
|
+
|
|
60
|
+
```http
|
|
61
|
+
GET /signalk/v2/api/history/values?paths=navigation.speedOverGround&duration=PT1H&resolution=60
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
A `navigation.position` value is a `[longitude, latitude]` pair — GeoJSON order,
|
|
65
|
+
as the History API defines it and as signalk-to-influxdb2 and signalk-parquet
|
|
66
|
+
return it — not the `{latitude, longitude}` object of the data model; a bucket
|
|
67
|
+
without a fix is `null`. The v1 playback API is unaffected: it replays deltas,
|
|
68
|
+
which carry the object.
|
|
69
|
+
|
|
70
|
+
**Limits.** A request may produce at most 1 000 000 sample buckets across all of
|
|
71
|
+
its columns (range ÷ resolution × columns); a finer one is rejected with an
|
|
72
|
+
error that says so — use a coarser resolution or a shorter range. Without a
|
|
73
|
+
resolution a column is its raw rows, capped at 10 000 (50 000 for
|
|
74
|
+
`middle_index`).
|
|
75
|
+
|
|
76
|
+
### Reading one source
|
|
77
|
+
|
|
78
|
+
Append `|<sourceRef>` to a path to read one source's rows only (server 2.29+,
|
|
79
|
+
[signalk-server#2737](https://github.com/SignalK/signalk-server/pull/2737)). The
|
|
80
|
+
same path may appear once per source, giving one column per receiver:
|
|
81
|
+
|
|
82
|
+
```http
|
|
83
|
+
GET /signalk/v2/api/history/values?paths=navigation.position|gps.main,navigation.position|gps.backup&duration=PT1H
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Without a sourceRef a path returns all sources mixed. A source-bearing column
|
|
87
|
+
reports its source as `$source` in its `values` entry — the key
|
|
88
|
+
[signalk-server#2817](https://github.com/SignalK/signalk-server/pull/2817)
|
|
89
|
+
settled on, and what v1 playback has always emitted — while the request side
|
|
90
|
+
keeps the name `sourceRef`.
|
|
91
|
+
|
|
92
|
+
### sourcePolicy=all
|
|
93
|
+
|
|
94
|
+
`sourcePolicy=all`
|
|
95
|
+
([signalk-server#2817](https://github.com/SignalK/signalk-server/pull/2817))
|
|
96
|
+
asks for every source separated without naming them up front. Each path that
|
|
97
|
+
does not already specify a source is expanded into one column per source that
|
|
98
|
+
actually recorded it in range, with the source in the `$source` field of the
|
|
99
|
+
column's `values` entry:
|
|
100
|
+
|
|
101
|
+
```http
|
|
102
|
+
GET /signalk/v2/api/history/values?paths=navigation.speedOverGround&duration=PT1H&sourcePolicy=all
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```jsonc
|
|
106
|
+
"values": [
|
|
107
|
+
{ "path": "navigation.speedOverGround", "method": "average", "$source": "gps.aux" },
|
|
108
|
+
{ "path": "navigation.speedOverGround", "method": "average", "$source": "gps.main" },
|
|
109
|
+
{ "path": "navigation.speedOverGround", "method": "average" } // unattributed rows
|
|
110
|
+
]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Named sources come first, sorted, so column order is stable between requests.
|
|
114
|
+
Rows whose source is unset — recorded before the `source` column existed, or
|
|
115
|
+
from a delta that carried none — form their own trailing column with no
|
|
116
|
+
`$source` claim.
|
|
117
|
+
|
|
118
|
+
**Off by default**, behind the plugin setting that allows the policy. Expansion
|
|
119
|
+
multiplies the work one request can ask for: a path recorded by four receivers
|
|
120
|
+
becomes four queries and four columns, which on a Pi-class host turns a cheap
|
|
121
|
+
request into an expensive one. The sample-bucket cap counts expanded columns, so
|
|
122
|
+
a resolution that is fine unexpanded may be rejected once expanded.
|
|
123
|
+
|
|
124
|
+
Two ceilings bound the fan-out, and a request past either is rejected rather
|
|
125
|
+
than truncated — returning some of the asked-for series without saying so would
|
|
126
|
+
be worse than refusing a request that is too broad. A single path may expand
|
|
127
|
+
into at most 16 columns: recorded by more sources in range, it fails the
|
|
128
|
+
request, and the error names the count, the ceiling and the way out (name the
|
|
129
|
+
sources with `path|sourceRef`, or ask for a shorter range). A whole request is
|
|
130
|
+
bounded at 64 columns, with its own error naming the limit.
|
|
131
|
+
|
|
132
|
+
An explicit `path|sourceRef` stays a filter and takes precedence over the
|
|
133
|
+
policy, matching the upstream contract. Requests that name their sources are
|
|
134
|
+
unaffected by the setting either way.
|
|
135
|
+
|
|
136
|
+
## v1 (WebSocket playback)
|
|
137
|
+
|
|
138
|
+
Registered via `app.registerHistoryProvider()`. Supports playback at
|
|
139
|
+
configurable speed multipliers using chunked reads from QuestDB. Replayed
|
|
140
|
+
updates carry the recorded sourceRef as `$source`, one update per source, so
|
|
141
|
+
consumers see the same attribution the live stream had.
|
|
142
|
+
|
|
143
|
+
## REST endpoints
|
|
144
|
+
|
|
145
|
+
All under `/plugins/signalk-questdb/api/`; the console is mounted beside them at
|
|
146
|
+
`/plugins/signalk-questdb/console/`. Plugin routes are admin-only by Signal K's
|
|
147
|
+
default.
|
|
148
|
+
|
|
149
|
+
| Method | Path | Description |
|
|
150
|
+
| ------ | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| GET | `/status` | QuestDB health, row counts, active paths, restore outcome, WAL state, unstorable paths |
|
|
152
|
+
| GET | `/query?sql=...` | Read-only SQL proxy (DDL/DML blocked) |
|
|
153
|
+
| GET | `/paths` | All recorded paths with row counts and time range |
|
|
154
|
+
| GET | `/versions` | QuestDB releases from GitHub (for the version picker) |
|
|
155
|
+
| GET | `/update/check` | Compare the running version against the latest release |
|
|
156
|
+
| POST | `/update/apply` | Pull the latest image, recreate the container, reconnect |
|
|
157
|
+
| GET | `/wal-diagnosis` | Suspended tables with the engine's reason and a bounded skip plan (see [WAL suspension](deployment.md#wal-suspension)) |
|
|
158
|
+
| POST | `/resume-wal` | Lossless `RESUME WAL` of the suspended tables |
|
|
159
|
+
| POST | `/resume-wal/skip` | Guided skip of the unreadable segment — explicit, quantified, and only on request |
|
|
160
|
+
| POST | `/purge-data` | Remove the QuestDB container and delete all its data (rootless-Podman-safe) |
|
|
161
|
+
| GET | `/migration/detect` | Detect InfluxDB (supports `?url=` for a remote one) |
|
|
162
|
+
| POST | `/migration/buckets` | List buckets (2.x) or databases (1.x); credentials in the body, never the query string |
|
|
163
|
+
| POST | `/migration/measurements` | List measurements and their field keys in a bucket/database |
|
|
164
|
+
| POST | `/migration/contexts` | List the vessels a bucket/database holds, and which is this server's |
|
|
165
|
+
| POST | `/migration/start` | Start an import, or continue a stopped one with `{"resume": true}`; returns immediately |
|
|
166
|
+
| GET | `/migration/status` | Progress and state of the current/last import, and any stopped import that can resume |
|
|
167
|
+
| POST | `/migration/cancel` | Cancel the running import; any saved position is kept |
|
|
168
|
+
| POST | `/migration/discard` | Forget a stopped import's saved position, so the next start begins again |
|
|
169
|
+
| GET | `/migration/legacy-rows` | Count rows an import made before 2.1.5 filed under suffixed paths |
|
|
170
|
+
| POST | `/migration/legacy-rows/remove` | Rebuild the string table without those rows; recording continues meanwhile |
|
|
171
|
+
| GET | `/export?from=...&to=...&format=parquet` | Parquet or CSV export of the `signalk` numeric table (date range required) |
|
|
172
|
+
| GET | `/full-export/tables` | List the tables the full-export route serves |
|
|
173
|
+
| GET | `/full-export/:table?from=...&to=...` | Stream a table as Parquet; optional half-open `[from, to)` window for slicing into shards |
|
|
174
|
+
| GET | `/gpx-import` | The current or last GPX import |
|
|
175
|
+
| POST | `/gpx-import` | Announce an import: `{filename, size, tracks, fillGapsOnly}`, see [Importing a GPX track](import-export.md#importing-a-gpx-track) |
|
|
176
|
+
| PUT | `/gpx-import/:id?offset=N` | One slice of the file, as `application/octet-stream`, at most 8 MiB |
|
|
177
|
+
| POST | `/gpx-import/:id/finish` | End the import once every byte is in; answers when QuestDB has applied it |
|
|
178
|
+
| DELETE | `/gpx-import/:id` | Abort the import; what it wrote stays |
|