pmtiles-swarm 0.98.3 → 0.99.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.99.0
11
+ ### ✨ Features and improvements
12
+ - **A sidecar crash now names the frame it happened in.** `pmtiles-torrent` 0.11.0 arms
13
+ `faulthandler` in the sidecar, so a segfault writes a Python traceback to stderr on its way out
14
+ and this node forwards it into the log line by line, beside the `sidecar killed by SIGSEGV` that
15
+ used to be the whole story. Nothing to configure; it costs nothing until the process faults.
16
+
17
+ Worth having because that signal is otherwise the only evidence. On the node this came from it
18
+ appeared several times per start for eleven days, and each crash takes the archives handed over
19
+ before it — which is what leaves a library reading `not loaded` after a restart.
20
+
21
+ ### 🐞 Bug fixes
22
+ - **An archive the engine took and then did not keep is handed back, rather than only reported.**
23
+ The seeding check has been able to spot this for a while and its own message admitted the rest:
24
+ "nothing will start it before the next restart". It was right, which is why the cure was always
25
+ another restart. The usual cause is a sidecar that dies partway through a restore — the
26
+ replacement holds nothing, so the archives handed over before it died are absent while the ones
27
+ after it are fine, and `add` resolving is no evidence that anything was kept.
28
+
29
+ Those are now handed back once and re-checked, and the log says whether it took. Once, not in a
30
+ loop: an engine that refuses twice will not be talked round by a third try. And only when the
31
+ engine is holding *some* of the library — one holding none of it, or one that could not be listed
32
+ at all, is not suffering a per-archive fault, and re-adding everything on the strength of that
33
+ answer is how a node spends its start hashing what it already had.
34
+ - **A pinned archive in a stack was listed by its infohash.** Forty characters of hex, where the
35
+ picker that offered it had shown a filename. The row leads with the archive's name now and shows
36
+ the infohash as what it resolves to, which for a pinned source is exactly what it means: this
37
+ build and no later one.
38
+
10
39
  ## 0.98.3
11
40
  ### ✨ Features and improvements
12
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.98.3",
3
+ "version": "0.99.0",
4
4
  "description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -49,7 +49,7 @@
49
49
  "maplibre-gl": "^6.2.0",
50
50
  "parse-torrent": "^11.0.24",
51
51
  "pmtiles": "^4.4.1",
52
- "pmtiles-torrent": "^0.10.2",
52
+ "pmtiles-torrent": "^0.11.0",
53
53
  "webtorrent": "^3.0.21"
54
54
  },
55
55
  "engines": {
package/src/library.js CHANGED
@@ -2154,16 +2154,46 @@ export class Library {
2154
2154
  * @param {object[]} entries - The entries restore worked through.
2155
2155
  * @returns {Promise<void>} - Resolves once every claim has been checked.
2156
2156
  */
2157
+ /**
2158
+ * Whether the engine is holding this archive right now.
2159
+ *
2160
+ * Asked of one archive rather than by re-listing the library: this runs only
2161
+ * for an archive that has just been handed back, which on a healthy node is
2162
+ * never.
2163
+ * @param {string} infoHash - The archive.
2164
+ * @returns {Promise<boolean>} - Whether the engine has it.
2165
+ */
2166
+ async #engineHolds(infoHash) {
2167
+ const status = await this.#engine.get?.(infoHash).catch(() => null);
2168
+ return Boolean(status);
2169
+ }
2170
+
2157
2171
  async #verifySeeding(entries) {
2158
2172
  if (entries.length === 0) return;
2159
2173
 
2160
2174
  // One listing rather than a status call each: this runs over the whole
2161
2175
  // library on every start, and a round trip per archive is a cost paid by
2162
2176
  // every node to catch a fault most of them do not have.
2163
- const held = new Map();
2164
- for (const status of await this.#engine.list().catch(() => [])) {
2165
- held.set(status.infoHash, status);
2166
- }
2177
+ // Whether the listing worked is a separate fact from what it contained,
2178
+ // and collapsing the two into an empty map is what made this only ever
2179
+ // able to report. A repair has to know the difference: an engine that
2180
+ // could not be asked is not an engine holding nothing.
2181
+ const listing = await this.#engine.list().then(
2182
+ (all) => ({ answered: true, all }),
2183
+ () => ({ answered: false, all: [] }),
2184
+ );
2185
+ const held = new Map(
2186
+ listing.all.map((status) => [status.infoHash, status]),
2187
+ );
2188
+
2189
+ // Handing archives back one at a time only makes sense against an engine
2190
+ // that is holding some of them. One holding none of what it was just given
2191
+ // is not suffering a per-archive fault -- it is a replacement that came up
2192
+ // empty, or a listing that cannot be trusted -- and re-adding the whole
2193
+ // library on the strength of a bad answer is how a node spends its start
2194
+ // hashing everything it already had. That case is the reconnect handler's,
2195
+ // and it is reported here rather than acted on.
2196
+ const repairable = listing.answered && held.size > 0;
2167
2197
 
2168
2198
  let wrong = 0;
2169
2199
  for (const entry of entries) {
@@ -2178,11 +2208,50 @@ export class Library {
2178
2208
  // it absent from the engine and unreported by the very check meant to
2179
2209
  // notice. Absent is absent — it is neither seeding nor downloading.
2180
2210
  if (!status) {
2211
+ // Handed back rather than only reported. This is the one fault here
2212
+ // with an obvious remedy -- the archive was restorable a moment ago,
2213
+ // since restore did it without complaint -- and the usual cause is a
2214
+ // sidecar that died partway through and was replaced by one holding
2215
+ // nothing. The replacement is up by the time this runs, so the second
2216
+ // attempt is against a working engine.
2217
+ //
2218
+ // Once, not in a loop. An engine that refuses twice is not going to be
2219
+ // talked round by a third try, and a restore that retried for ever
2220
+ // would keep a node busy instead of letting it say what is wrong.
2221
+ if (!repairable) {
2222
+ wrong += 1;
2223
+ console.error(
2224
+ `${label}: restore handed this to the engine and the engine is ` +
2225
+ 'not holding it. It is neither seeding nor downloading, and ' +
2226
+ 'nothing will start it before the next restart.',
2227
+ );
2228
+ continue;
2229
+ }
2230
+
2231
+ const again = await this.#readd(entry).then(
2232
+ () => true,
2233
+ (error) => {
2234
+ console.error(
2235
+ `${label}: could not be handed back: ${error.message}`,
2236
+ );
2237
+ return false;
2238
+ },
2239
+ );
2240
+ const recovered = again && (await this.#engineHolds(entry.infoHash));
2241
+ if (recovered) {
2242
+ console.warn(
2243
+ `${label}: the engine was not holding this after restore, so it ` +
2244
+ 'was handed back. It is loaded now.',
2245
+ );
2246
+ continue;
2247
+ }
2248
+
2181
2249
  wrong += 1;
2182
2250
  console.error(
2183
- `${label}: restore handed this to the engine and the engine is not ` +
2184
- 'holding it. It is neither seeding nor downloading, and nothing ' +
2185
- 'will start it before the next restart.',
2251
+ `${label}: restore handed this to the engine, the engine is not ` +
2252
+ 'holding it, and handing it back did not take either. It is ' +
2253
+ 'neither seeding nor downloading. The log above this says what ' +
2254
+ 'the engine has been doing; a restart is the next thing to try.',
2186
2255
  );
2187
2256
  continue;
2188
2257
  }
@@ -7963,6 +7963,17 @@ Every piece is hashed against the ` +
7963
7963
  ? source.bounds.map((n) => Number(n).toFixed(2)).join(', ')
7964
7964
  : null;
7965
7965
 
7966
+ // A pinned archive is named in the recipe by its infohash, which is
7967
+ // forty characters of hex and tells nobody which archive it is. The
7968
+ // filename is what the picker offered and what an operator recognises,
7969
+ // so lead with that and let the infohash be what it resolves to --
7970
+ // which for a pinned source is exactly what it means: this build, and
7971
+ // no later one. Falls back to the hash for an archive that has been
7972
+ // removed, where the name is all that is gone.
7973
+ const pinnedArchive = kind === 'archive';
7974
+ const label =
7975
+ pinnedArchive && source.archiveName ? source.archiveName : source.name;
7976
+
7966
7977
  const notes = [];
7967
7978
  if (index === all.length - 1 && all.length > 1) {
7968
7979
  notes.push('<span class="pill">wins</span>');
@@ -7975,7 +7986,7 @@ Every piece is hashed against the ` +
7975
7986
  return `
7976
7987
  <tr${source.resolved ? '' : ' class="bad"'}>
7977
7988
  <td class="muted">${index}</td>
7978
- <td><code>${escapeHtml(source.name)}</code>
7989
+ <td><code>${escapeHtml(label)}</code>
7979
7990
  <span class="muted">${kind}</span></td>
7980
7991
  <td>${
7981
7992
  source.resolved
@@ -7985,7 +7996,13 @@ Every piece is hashed against the ` +
7985
7996
  ? `<span class="muted" title="Another recipe, evaluated for this tile and merged as one layer. It follows every later change to that stack.">recipe of ${
7986
7997
  source.nested ?? 0
7987
7998
  } source${source.nested === 1 ? '' : 's'}</span>`
7988
- : `<code class="muted">${escapeHtml(source.archiveName ?? '')}</code>`
7999
+ : pinnedArchive
8000
+ ? `<code class="muted" title="${escapeHtml(
8001
+ source.infohash ?? source.name,
8002
+ )}">${escapeHtml(
8003
+ (source.infohash ?? source.name).slice(0, 12),
8004
+ )}…</code>`
8005
+ : `<code class="muted">${escapeHtml(source.archiveName ?? '')}</code>`
7989
8006
  : '<span class="bad">does not resolve</span>'
7990
8007
  }</td>
7991
8008
  <td>${zooms}${