pmtiles-swarm 0.41.0 → 0.41.1

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,27 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.41.1
11
+ ### ✨ Features and improvements
12
+
13
+ ### 🐞 Bug fixes
14
+ - **`KillMode=mixed` in the documented unit, and it should have been there from the start.** systemd's
15
+ default is `control-group`, which sends `SIGTERM` to every process in the unit at the same instant —
16
+ the node and the Python sidecar together. The node's shutdown then asks a sidecar that is already
17
+ dying to write its resume data, into a pipe that is closing, and the answer never comes.
18
+
19
+ Resume data is how an archive comes back knowing what it holds. Without it, archives return at 0%
20
+ after a restart and only a recheck discovers they were complete all along. `mixed` signals the main
21
+ process only; the node stops the sidecar itself and waits for the write. Nothing is left running —
22
+ anything still alive at `TimeoutStopSec` is killed, sidecar included. **Existing installs need this
23
+ added by hand**, followed by `systemctl daemon-reload`.
24
+ - **The check added in 0.40.0 asked its first question only of archives recorded as complete**, and so
25
+ missed the case that prompted it. An archive interrupted mid-download comes back recorded as
26
+ incomplete, so a restore that failed to hand it over left it absent from the engine and unreported
27
+ by the very check meant to notice — which is what a row reading 0% with no state at all is. Whether
28
+ the engine is holding an archive is now asked of everything restore handed over; whether the
29
+ `seedOnly` claim held is still asked only of the archives that make one.
30
+
10
31
  ## 0.41.0
11
32
  ### ✨ Features and improvements
12
33
  - **An archive's details now open directly under the row you clicked**, instead of below the whole
@@ -180,6 +180,11 @@ RestartSec=5
180
180
  # lock and cancels downloads in flight. Worst case is about 20 seconds.
181
181
  TimeoutStopSec=45
182
182
 
183
+ # The node stops the sidecar itself, and needs it alive to do so. The default
184
+ # signals both at once, which kills the sidecar before it can write its resume
185
+ # data — see "The two lines that matter".
186
+ KillMode=mixed
187
+
183
188
  # A seeding node holds a socket per peer, and the tile reader holds file
184
189
  # descriptors of its own.
185
190
  LimitNOFILE=65535
@@ -215,12 +220,23 @@ down and **exits 0**, expecting to be brought back.
215
220
  `Restart=on-failure` ignores an exit 0, so the first use of that button would
216
221
  stop the node and leave the unit reporting success.
217
222
 
223
+ **`KillMode=mixed`, not the default.** The default is `control-group`, which
224
+ sends `SIGTERM` to _every_ process in the unit at the same instant — the node
225
+ and the Python sidecar together. The node's shutdown then asks a sidecar that is
226
+ already dying to write its resume data, into a pipe that is closing, and the
227
+ answer never comes. Resume data is how an archive comes back knowing what it
228
+ holds; without it, archives return at 0% after a restart and have to be
229
+ rechecked to find out they were complete all along.
230
+
231
+ `mixed` sends the signal to the main process only. The node stops the sidecar
232
+ itself, in order, and waits for the resume data to be written. Nothing is left
233
+ running: anything still alive when `TimeoutStopSec` expires is killed, sidecar
234
+ included.
235
+
218
236
  **No `ExecStop=`.** systemd already sends `SIGTERM`, and the node handles it
219
237
  from the moment it starts. `ExecStop=/bin/kill -15 $MAINPID` is redundant, and
220
- becomes wrong if the unit ever uses `KillMode=process` — it would stop the node
221
- while the Python sidecar kept running and kept the data directory locked.
222
-
223
- Leave `KillMode` at its default, so the sidecar goes with its parent.
238
+ becomes wrong under `KillMode=process` — that one leaves the rest of the unit
239
+ running indefinitely, so the sidecar would keep the data directory locked.
224
240
 
225
241
  ## Where it writes
226
242
 
@@ -587,8 +603,10 @@ and lives as long as it does, so a new sidecar sits on disk doing nothing until
587
603
  the service is restarted. Most of what changes between releases is in there.
588
604
 
589
605
  Nothing under `/etc/pmtiles-swarm` is touched, and restarting does not re-check
590
- the archives: a clean stop writes resume data, and `TimeoutStopSec` above
591
- leaves room for it.
606
+ the archives: a clean stop writes resume data, and `TimeoutStopSec` above leaves
607
+ room for it. That depends on `KillMode=mixed` — without it the sidecar is
608
+ signalled at the same moment as the node and dies before it can write anything,
609
+ which is what a library that comes back at 0% after every restart looks like.
592
610
 
593
611
  Confirm both halves moved, since the sidecar has its own version:
594
612
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
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",
package/src/library.js CHANGED
@@ -1840,10 +1840,7 @@ export class Library {
1840
1840
  * @returns {Promise<void>} - Resolves once every claim has been checked.
1841
1841
  */
1842
1842
  async #verifySeeding(entries) {
1843
- const claimed = entries.filter(
1844
- (entry) => entry.complete !== false && entry.mode !== 'cache',
1845
- );
1846
- if (claimed.length === 0) return;
1843
+ if (entries.length === 0) return;
1847
1844
 
1848
1845
  // One listing rather than a status call each: this runs over the whole
1849
1846
  // library on every start, and a round trip per archive is a cost paid by
@@ -1854,25 +1851,37 @@ export class Library {
1854
1851
  }
1855
1852
 
1856
1853
  let wrong = 0;
1857
- for (const entry of claimed) {
1854
+ for (const entry of entries) {
1858
1855
  const status = held.get(entry.infoHash);
1859
-
1860
- // Checking is the engine doing the right thing already, and progress
1861
- // during it is the fraction hashed rather than the fraction held.
1862
- if (status && (status.progress >= 1 || status.state === 'checking')) {
1863
- continue;
1864
- }
1865
- wrong += 1;
1866
1856
  const label = `[seeding] ${entry.name}`;
1867
1857
 
1858
+ // Held at all is a different question from held whole, and it is asked
1859
+ // of everything restore handed over. Asking it only of complete archives
1860
+ // was the first version of this check, and it missed the case that
1861
+ // prompted it: an archive interrupted mid-download comes back recorded
1862
+ // as incomplete, so a restore that silently failed to hand it over left
1863
+ // it absent from the engine and unreported by the very check meant to
1864
+ // notice. Absent is absent — it is neither seeding nor downloading.
1868
1865
  if (!status) {
1866
+ wrong += 1;
1869
1867
  console.error(
1870
- `${label}: recorded as complete, but the engine is not holding it ` +
1871
- 'at all — restore counted it and nothing is seeding it.',
1868
+ `${label}: restore handed this to the engine and the engine is not ` +
1869
+ 'holding it. It is neither seeding nor downloading, and nothing ' +
1870
+ 'will start it before the next restart.',
1872
1871
  );
1873
1872
  continue;
1874
1873
  }
1875
1874
 
1875
+ // Everything below is about the `seedOnly` claim, and only a complete
1876
+ // archive makes one. An incomplete one is supposed to read as a partial
1877
+ // download, so its progress says nothing about whether anything is wrong.
1878
+ if (entry.complete === false || entry.mode === 'cache') continue;
1879
+
1880
+ // Checking is the engine doing the right thing already, and progress
1881
+ // during it is the fraction hashed rather than the fraction held.
1882
+ if (status.progress >= 1 || status.state === 'checking') continue;
1883
+ wrong += 1;
1884
+
1876
1885
  const file = entry.savePath
1877
1886
  ? path.join(entry.savePath, entry.name)
1878
1887
  : null;
@@ -1921,8 +1930,8 @@ export class Library {
1921
1930
 
1922
1931
  if (wrong > 0) {
1923
1932
  console.error(
1924
- `[seeding] ${wrong} of ${claimed.length} archives recorded as ` +
1925
- 'complete are not being seeded. The lines above say which and why.',
1933
+ `[seeding] ${wrong} of ${entries.length} restored archives are not ` +
1934
+ 'in the state the catalog describes. The lines above say which and why.',
1926
1935
  );
1927
1936
  }
1928
1937
  }