pmtiles-swarm 0.5.2 → 0.5.4
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 +33 -0
- package/package.json +1 -1
- package/src/config.js +20 -5
- package/src/prewarm.js +75 -11
- package/src/read-engine.js +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.5.4
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **The head-warm waits sensibly at both ends.** It used to read at the instant the node
|
|
13
|
+
started — when an archive joined by magnet has no metainfo, the engine has no peers, and the
|
|
14
|
+
attempt is certain to find nothing — and then leave a flat two minutes between every try
|
|
15
|
+
afterwards. Neither suited what was actually being waited for. The first pass now comes ten
|
|
16
|
+
seconds in, and the wait after an attempt that did not finish starts at fifteen seconds and
|
|
17
|
+
doubles to a ten-minute ceiling: seconds early on, when what is missing is usually a peer or a
|
|
18
|
+
piece already in flight, and minutes later on, when it is one piece at the far end of an
|
|
19
|
+
archive nobody has finished downloading.
|
|
20
|
+
|
|
21
|
+
New under `tiles`: `prewarmInitialDelaySeconds` (10) and `prewarmMaxBackoffSeconds` (600).
|
|
22
|
+
`prewarmBackoffSeconds` is now the *first* wait rather than every wait, and defaults to 15.
|
|
23
|
+
|
|
24
|
+
## 0.5.3
|
|
25
|
+
### 🐞 Bug fixes
|
|
26
|
+
- **A "high" priority hint asked for nothing at all.** libtorrent's scale runs 0 to 7 and **4 is
|
|
27
|
+
the default every piece already has** — and high was mapped to 4. So the JSON metadata, which
|
|
28
|
+
the source hints the moment it reads a header, was raised to precisely what the other eighteen
|
|
29
|
+
thousand pieces already had, and waited its turn: hours, on a 72 GiB archive. It is 6 now,
|
|
30
|
+
above the default and below critical, so it arrives soon without ever going ahead of a tile
|
|
31
|
+
somebody is waiting for. `normal` stays at 1, deliberately below the default, because that is
|
|
32
|
+
what the idle leaf hydration uses and it must yield to everything.
|
|
33
|
+
|
|
34
|
+
There is a test for the mapping itself, since a priority that is merely ordinary fails by
|
|
35
|
+
doing nothing, which no behavioural test would notice.
|
|
36
|
+
- **A partial head-read no longer reports itself as a complete one.** The header sits at byte
|
|
37
|
+
zero while the JSON metadata is wherever the writer put it — planetiler puts it after every
|
|
38
|
+
tile, so at the far end of the file — and `summarize` treats the second as decoration, so a
|
|
39
|
+
pass that got only the header still returned a summary and logged "read the head". It then
|
|
40
|
+
came back two minutes later, having genuinely succeeded and genuinely not finished, with
|
|
41
|
+
nothing in the log to explain the repetition. The two are now reported separately.
|
|
42
|
+
|
|
10
43
|
## 0.5.2
|
|
11
44
|
### 🐞 Bug fixes
|
|
12
45
|
- **Asks for the `pmtiles-torrent` that 0.5.1 actually needs.** It shipped declaring `^0.3.2`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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/config.js
CHANGED
|
@@ -460,13 +460,28 @@ const DEFAULTS = {
|
|
|
460
460
|
/** How often to look for an archive whose head has not been read. */
|
|
461
461
|
prewarmIntervalSeconds: 30,
|
|
462
462
|
/**
|
|
463
|
-
* How long to
|
|
463
|
+
* How long to let the node settle before the first attempt.
|
|
464
464
|
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
465
|
+
* At the moment a node starts, an archive joined by magnet has no metainfo
|
|
466
|
+
* and the engine has no peers, so a read attempted immediately is certain
|
|
467
|
+
* to find nothing.
|
|
468
468
|
*/
|
|
469
|
-
|
|
469
|
+
prewarmInitialDelaySeconds: 10,
|
|
470
|
+
/**
|
|
471
|
+
* The first wait after an attempt that did not finish the job, doubling up
|
|
472
|
+
* to `prewarmMaxBackoffSeconds`.
|
|
473
|
+
*
|
|
474
|
+
* Not finishing is ordinary rather than exceptional here, so this is a
|
|
475
|
+
* retry interval and not an error budget — and it doubles because the two
|
|
476
|
+
* ends of the problem want different answers. Just after a start, what is
|
|
477
|
+
* being waited for is usually seconds away: a peer, a connection, a piece
|
|
478
|
+
* already in flight. Ten attempts later it is one piece at the far end of
|
|
479
|
+
* an archive nobody has finished, and asking every fifteen seconds
|
|
480
|
+
* achieves nothing but log lines.
|
|
481
|
+
*/
|
|
482
|
+
prewarmBackoffSeconds: 15,
|
|
483
|
+
/** Where the doubling stops. */
|
|
484
|
+
prewarmMaxBackoffSeconds: 600,
|
|
470
485
|
/**
|
|
471
486
|
* What a missing tile answers with: true for 404, false for 204.
|
|
472
487
|
*
|
package/src/prewarm.js
CHANGED
|
@@ -21,8 +21,14 @@
|
|
|
21
21
|
* the first few seconds if the right few kilobytes are asked for first.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
24
|
+
/** The first wait after an attempt that did not finish the job. */
|
|
25
|
+
const DEFAULT_BACKOFF_SECONDS = 15;
|
|
26
|
+
|
|
27
|
+
/** Where the doubling stops. */
|
|
28
|
+
const DEFAULT_MAX_BACKOFF_SECONDS = 600;
|
|
29
|
+
|
|
30
|
+
/** How long to let the node settle before the first attempt. */
|
|
31
|
+
const DEFAULT_INITIAL_DELAY_SECONDS = 10;
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Whether a failure means "not yet" rather than "not working".
|
|
@@ -49,6 +55,7 @@ export class HeadWarmer {
|
|
|
49
55
|
#now;
|
|
50
56
|
#timer;
|
|
51
57
|
#tried = new Map();
|
|
58
|
+
#attempts = new Map();
|
|
52
59
|
#waiting = new Set();
|
|
53
60
|
#running = false;
|
|
54
61
|
|
|
@@ -93,8 +100,30 @@ export class HeadWarmer {
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const last = this.#tried.get(entry.infoHash) ?? 0;
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
return this.#now() - last >= this.#backoffFor(entry.infoHash);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* How long to wait before trying this archive again.
|
|
108
|
+
*
|
|
109
|
+
* Doubling, from a short first wait to a long ceiling. A flat interval is
|
|
110
|
+
* wrong at both ends: right after a node starts, the thing being waited for
|
|
111
|
+
* is usually a few seconds away — a peer, a connection, a piece already in
|
|
112
|
+
* flight — so a two-minute wait wastes most of it. Ten attempts later the
|
|
113
|
+
* thing being waited for is a single piece at the far end of an archive
|
|
114
|
+
* nobody has finished downloading, and asking every two minutes achieves
|
|
115
|
+
* nothing but log lines.
|
|
116
|
+
* @param {string} infoHash - The archive.
|
|
117
|
+
* @returns {number} - Milliseconds to wait.
|
|
118
|
+
*/
|
|
119
|
+
#backoffFor(infoHash) {
|
|
120
|
+
const base =
|
|
121
|
+
(this.#config.tiles?.prewarmBackoffSeconds ?? DEFAULT_BACKOFF_SECONDS) * 1000;
|
|
122
|
+
const ceiling =
|
|
123
|
+
(this.#config.tiles?.prewarmMaxBackoffSeconds ?? DEFAULT_MAX_BACKOFF_SECONDS) *
|
|
124
|
+
1000;
|
|
125
|
+
const attempts = this.#attempts.get(infoHash) ?? 0;
|
|
126
|
+
return Math.min(base * 2 ** Math.max(0, attempts - 1), ceiling);
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
/**
|
|
@@ -123,17 +152,38 @@ export class HeadWarmer {
|
|
|
123
152
|
|
|
124
153
|
this.#tried.set(entry.infoHash, this.#now());
|
|
125
154
|
this.#waiting.delete(entry.infoHash);
|
|
155
|
+
// Counted even on success, because a read that got the header and not
|
|
156
|
+
// the metadata has not finished and will be back. The count only matters
|
|
157
|
+
// while an archive is still due, and one that is done is never asked
|
|
158
|
+
// about again.
|
|
159
|
+
this.#attempts.set(
|
|
160
|
+
entry.infoHash,
|
|
161
|
+
(this.#attempts.get(entry.infoHash) ?? 0) + 1,
|
|
162
|
+
);
|
|
126
163
|
|
|
127
164
|
const stored = await this.#catalog.put({
|
|
128
165
|
infoHash: entry.infoHash,
|
|
129
166
|
pmtiles: { ...entry.pmtiles, ...summary },
|
|
130
167
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
// Said accurately, because the two halves arrive separately and the
|
|
169
|
+
// difference matters: the header is at byte zero, while the JSON
|
|
170
|
+
// metadata is wherever the writer put it — planetiler puts it after every
|
|
171
|
+
// tile, so on a 72 GiB archive it is the very end of the file. Reporting
|
|
172
|
+
// both as "read the head" made a pass that got half of it look complete,
|
|
173
|
+
// and left the repeat every couple of minutes unexplained.
|
|
174
|
+
if (summary.vectorLayers) {
|
|
175
|
+
console.log(
|
|
176
|
+
`[warm] ${entry.name}: header and metadata read ` +
|
|
177
|
+
`(${summary.vectorLayers.length} vector layers)`,
|
|
178
|
+
);
|
|
179
|
+
} else if (summary.format === 'pbf') {
|
|
180
|
+
console.log(
|
|
181
|
+
`[warm] ${entry.name}: header read; its metadata is at the far end ` +
|
|
182
|
+
'of the archive and has not arrived yet',
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(`[warm] ${entry.name}: header read`);
|
|
186
|
+
}
|
|
137
187
|
return stored;
|
|
138
188
|
} catch (error) {
|
|
139
189
|
if (tooEarly(error)) {
|
|
@@ -156,6 +206,10 @@ export class HeadWarmer {
|
|
|
156
206
|
// is my preview blank", and the backoff keeps it from becoming noise.
|
|
157
207
|
this.#tried.set(entry.infoHash, this.#now());
|
|
158
208
|
this.#waiting.delete(entry.infoHash);
|
|
209
|
+
this.#attempts.set(
|
|
210
|
+
entry.infoHash,
|
|
211
|
+
(this.#attempts.get(entry.infoHash) ?? 0) + 1,
|
|
212
|
+
);
|
|
159
213
|
console.warn(`[warm] ${entry.name}: ${error.message}`);
|
|
160
214
|
return null;
|
|
161
215
|
} finally {
|
|
@@ -176,7 +230,17 @@ export class HeadWarmer {
|
|
|
176
230
|
this.sweep().catch((error) =>
|
|
177
231
|
console.error(`[warm] sweep failed: ${error.message}`),
|
|
178
232
|
);
|
|
179
|
-
|
|
233
|
+
|
|
234
|
+
// Not immediately. At the moment a node starts, an archive joined by
|
|
235
|
+
// magnet has no metainfo, the engine has no peers, and nothing can be read
|
|
236
|
+
// from anywhere — so the first pass is guaranteed to find nothing and
|
|
237
|
+
// exists only to say so.
|
|
238
|
+
const delay =
|
|
239
|
+
(this.#config.tiles?.prewarmInitialDelaySeconds ??
|
|
240
|
+
DEFAULT_INITIAL_DELAY_SECONDS) * 1000;
|
|
241
|
+
const first = setTimeout(run, Math.max(0, delay));
|
|
242
|
+
first.unref?.();
|
|
243
|
+
|
|
180
244
|
this.#timer = setInterval(run, seconds * 1000);
|
|
181
245
|
this.#timer.unref?.();
|
|
182
246
|
}
|
package/src/read-engine.js
CHANGED
|
@@ -9,7 +9,21 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** libtorrent piece priorities. 0 means "do not fetch". */
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* What a hint's priority means to libtorrent.
|
|
14
|
+
*
|
|
15
|
+
* libtorrent's scale runs 0 (do not download) to 7 (highest), and **4 is the
|
|
16
|
+
* default every piece already has**. So "high" was mapped to the default: the
|
|
17
|
+
* JSON metadata, which the source hints as high the moment it reads the header,
|
|
18
|
+
* was never prioritised at all — it waited its turn like the other eighteen
|
|
19
|
+
* thousand pieces, which on a 72 GiB archive is hours.
|
|
20
|
+
*
|
|
21
|
+
* 6 is above the default and below critical, which is what "fetch this sooner
|
|
22
|
+
* than the rest, but not ahead of a tile somebody is waiting for" should mean.
|
|
23
|
+
* `normal` stays at 1, deliberately *below* the default: it is what the idle
|
|
24
|
+
* leaf hydration uses, and that must yield to everything.
|
|
25
|
+
*/
|
|
26
|
+
const LT_PRIORITY = { critical: 7, high: 6, normal: 1 };
|
|
13
27
|
|
|
14
28
|
/**
|
|
15
29
|
* A TorrentEngine over the libtorrent sidecar the seed engine already runs.
|