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 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
@@ -28,7 +28,11 @@ const { csvToLineProtocol } = require('./lineproto')
28
28
  const HOUR_MS = 3600000
29
29
  const DEFAULTS = {
30
30
  windowMs: HOUR_MS,
31
- batchSize: 10000,
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
- async function doWindow (startMs, stopMs) {
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
- return doWindow(startMs, mid)
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
- // Verify: a 204 says the bytes were accepted, not that every point landed. This is
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 r = await doWindow(startMs, stopMs)
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.1",
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": {