sailkick-boat 0.18.1 → 0.18.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/README.md +9 -0
- package/lib/backfill/index.js +36 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -236,6 +236,15 @@ a source archive that is still being written — a `signalk-to-influxdb-v2` buck
|
|
|
236
236
|
recording — makes the first windows re-upload today's data. The timestamps are correct,
|
|
237
237
|
but it is data the cloud already has, and a lot of wasted uplink.
|
|
238
238
|
|
|
239
|
+
**Latency is the cost, not bandwidth.** Measured on a real archive, about 3 s of every
|
|
240
|
+
4.2 s chunk was cloud round trips while all the boat-side work (query, parse, convert)
|
|
241
|
+
took ~1.2 s. So batches carry up to 50k lines rather than 10k — one or two round trips
|
|
242
|
+
per chunk instead of ten — and each **hour** is verified once rather than each of its
|
|
243
|
+
~32 chunks. The verification itself is unchanged in kind: a `204` means the bytes were
|
|
244
|
+
accepted, not that every point landed, so the destination is still counted before an
|
|
245
|
+
hour is marked done. On a mismatch the hour is left unmarked and simply redone, which is
|
|
246
|
+
safe because writes are idempotent.
|
|
247
|
+
|
|
239
248
|
**Field types come from the source, not from guessing.** Queries ask InfluxDB for the
|
|
240
249
|
`#datatype` annotation explicitly. Without it the response is unannotated and types have
|
|
241
250
|
to be inferred from the text — which fails hard on a string field whose values sometimes
|
package/lib/backfill/index.js
CHANGED
|
@@ -28,7 +28,11 @@ const { csvToLineProtocol } = require('./lineproto')
|
|
|
28
28
|
const HOUR_MS = 3600000
|
|
29
29
|
const DEFAULTS = {
|
|
30
30
|
windowMs: HOUR_MS,
|
|
31
|
-
|
|
31
|
+
// Latency dominates, not bandwidth: measured on a real run, ~3s of every ~4.2s chunk
|
|
32
|
+
// was cloud round trips while all the Pi-side work took ~1.2s. At 10k a 100k chunk
|
|
33
|
+
// cost TEN sequential TLS round trips. 50k halves the peak memory of the gzip buffer
|
|
34
|
+
// versus one giant batch while still cutting the round trips fivefold.
|
|
35
|
+
batchSize: 50000,
|
|
32
36
|
// A window is read whole and converted in memory. A busy archive can hold millions of
|
|
33
37
|
// points per hour (54M/day was measured on a real boat = ~2.25M/hour ~ 400 MB of CSV),
|
|
34
38
|
// which would exhaust a Raspberry Pi. When a window is denser than this it is halved
|
|
@@ -188,8 +192,10 @@ function createBackfill (app, options) {
|
|
|
188
192
|
|
|
189
193
|
// --- one window ---------------------------------------------------------------
|
|
190
194
|
// Returns 'done' | 'empty' | 'retry' | 'fatal' | 'stopped'. Subdivides itself when a
|
|
191
|
-
// window holds more points than can be held in memory at once.
|
|
192
|
-
|
|
195
|
+
// window holds more points than can be held in memory at once. Points written are
|
|
196
|
+
// added to `tally` so the CALLER can verify once per hour rather than once per chunk —
|
|
197
|
+
// an hour is ~32 chunks, and each verification is a cloud round trip.
|
|
198
|
+
async function doWindow (startMs, stopMs, tally) {
|
|
193
199
|
const srcCount = await count(cfg.src, cfg.src.bucket, startMs, stopMs)
|
|
194
200
|
if (srcCount == null) return 'retry'
|
|
195
201
|
if (srcCount === 0) return 'empty'
|
|
@@ -200,9 +206,11 @@ function createBackfill (app, options) {
|
|
|
200
206
|
// done, so a failure part-way is simply retried next time.
|
|
201
207
|
const mid = startMs + Math.floor((stopMs - startMs) / 2)
|
|
202
208
|
log(`${iso(startMs)} holds ${srcCount} points — splitting`)
|
|
203
|
-
const newer = await doWindow(mid, stopMs)
|
|
209
|
+
const newer = await doWindow(mid, stopMs, tally)
|
|
204
210
|
if (newer !== 'done' && newer !== 'empty') return newer
|
|
205
|
-
|
|
211
|
+
const older = await doWindow(startMs, mid, tally)
|
|
212
|
+
if (older !== 'done' && older !== 'empty') return older
|
|
213
|
+
return (newer === 'done' || older === 'done') ? 'done' : 'empty'
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
const filter = cfg._filter || ''
|
|
@@ -225,15 +233,7 @@ function createBackfill (app, options) {
|
|
|
225
233
|
}
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
|
|
229
|
-
// the check the write-only sync token could never perform.
|
|
230
|
-
const dstCount = await count(cfg.dst, cfg.dst.bucket, startMs, stopMs)
|
|
231
|
-
if (dstCount == null) { warn(`could not verify ${iso(startMs)} — leaving it for the next run`); return 'retry' }
|
|
232
|
-
if (dstCount < lines.length) {
|
|
233
|
-
warn(`${iso(startMs)} MISMATCH: wrote ${lines.length}, destination has ${dstCount} — not marking done`)
|
|
234
|
-
return 'retry'
|
|
235
|
-
}
|
|
236
|
-
if (state) state.points += lines.length
|
|
236
|
+
if (tally) tally.written += lines.length
|
|
237
237
|
return 'done'
|
|
238
238
|
}
|
|
239
239
|
|
|
@@ -300,13 +300,34 @@ function createBackfill (app, options) {
|
|
|
300
300
|
}
|
|
301
301
|
if (stopped) break
|
|
302
302
|
|
|
303
|
-
const
|
|
303
|
+
const tally = { written: 0 }
|
|
304
|
+
const r = await doWindow(startMs, stopMs, tally)
|
|
304
305
|
if (r === 'stopped') break
|
|
305
306
|
if (r === 'fatal') { statusLine = 'backfill: stopped — write rejected, see the log'; return }
|
|
306
307
|
if (r === 'retry') {
|
|
307
308
|
if (++errStreak >= cfg.maxErrorStreak) { warn(`${errStreak} consecutive failures — stopping this run, it resumes on restart`); statusLine = 'backfill: paused after repeated errors'; return }
|
|
308
309
|
continue
|
|
309
310
|
}
|
|
311
|
+
|
|
312
|
+
// Verify ONCE for the whole hour. A 204 says the bytes were accepted, not that
|
|
313
|
+
// every point landed, so this check still has to happen — but at the hour rather
|
|
314
|
+
// than the chunk it costs one cloud round trip instead of ~32. On a mismatch the
|
|
315
|
+
// hour is left unmarked and simply redone, which is safe because writes are
|
|
316
|
+
// idempotent. (>= not ==: the boundary hour also holds live-sync data.)
|
|
317
|
+
if (tally.written > 0) {
|
|
318
|
+
const dstCount = await count(cfg.dst, cfg.dst.bucket, startMs, stopMs)
|
|
319
|
+
if (dstCount == null) {
|
|
320
|
+
warn(`could not verify ${key} — leaving it for the next run`)
|
|
321
|
+
if (++errStreak >= cfg.maxErrorStreak) { statusLine = 'backfill: paused after repeated errors'; return }
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
if (dstCount < tally.written) {
|
|
325
|
+
warn(`${key} MISMATCH: wrote ${tally.written}, destination has ${dstCount} — not marking done`)
|
|
326
|
+
if (++errStreak >= cfg.maxErrorStreak) { statusLine = 'backfill: paused after repeated errors'; return }
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
state.points += tally.written
|
|
330
|
+
}
|
|
310
331
|
errStreak = 0
|
|
311
332
|
state.done[key] = r === 'empty' ? 'empty' : 'ok'
|
|
312
333
|
didWork++
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sailkick-boat",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "EARLY ALPHA — cloud telemetry + offline maps for sailkick boats (www.sailkick.io; register on the web, paste the write token). Gapless boat→cloud telemetry sync to InfluxDB, and a local proxy that keeps the sailkick app and its charts/maps working fully offline on board.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|