@teepin/sdk 0.3.0 → 0.4.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/dist/cjs/client.js +3 -1
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/direct/engine.js +49 -59
- package/dist/cjs/direct/engine.js.map +1 -1
- package/dist/cjs/direct/packing.js +549 -0
- package/dist/cjs/direct/packing.js.map +1 -0
- package/dist/cjs/direct/uploads.js +336 -64
- package/dist/cjs/direct/uploads.js.map +1 -1
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/direct/engine.js +45 -55
- package/dist/esm/direct/engine.js.map +1 -1
- package/dist/esm/direct/packing.js +536 -0
- package/dist/esm/direct/packing.js.map +1 -0
- package/dist/esm/direct/uploads.js +337 -65
- package/dist/esm/direct/uploads.js.map +1 -1
- package/dist/types/client.d.ts +1 -1
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/direct/engine.d.ts +37 -21
- package/dist/types/direct/engine.d.ts.map +1 -1
- package/dist/types/direct/packing.d.ts +207 -0
- package/dist/types/direct/packing.d.ts.map +1 -0
- package/dist/types/direct/uploads.d.ts +66 -13
- package/dist/types/direct/uploads.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -32,6 +32,7 @@ const bytes_js_1 = require("./bytes.js");
|
|
|
32
32
|
const engine_js_1 = require("./engine.js");
|
|
33
33
|
const common_js_1 = require("./common.js");
|
|
34
34
|
const hpke = __importStar(require("./hpke.js"));
|
|
35
|
+
const packing_js_1 = require("./packing.js");
|
|
35
36
|
const journal_js_1 = require("./journal.js");
|
|
36
37
|
const large_js_1 = require("./large.js");
|
|
37
38
|
const sha256_js_1 = require("./sha256.js");
|
|
@@ -170,6 +171,11 @@ class Item {
|
|
|
170
171
|
wake;
|
|
171
172
|
/** The pack and object id its file row last journaled. */
|
|
172
173
|
journaled = '';
|
|
174
|
+
/** A run's place for the file (items given back to the planner keep it). */
|
|
175
|
+
order = 0;
|
|
176
|
+
/** Rebuilds of the packs it was in, carried when a pack gives it back, so MAX_REBUILDS holds. */
|
|
177
|
+
rebuilds = 0;
|
|
178
|
+
reqBytes = 0;
|
|
173
179
|
contentType;
|
|
174
180
|
metadata;
|
|
175
181
|
ifMatch;
|
|
@@ -185,6 +191,16 @@ class Item {
|
|
|
185
191
|
this.metadata = { ...(metadata ?? {}) };
|
|
186
192
|
this.ifMatch = ifMatch || '';
|
|
187
193
|
}
|
|
194
|
+
/** Its bytes in a pack: its needle and TWS1 bytes. */
|
|
195
|
+
get stored() {
|
|
196
|
+
return tws1.NEEDLE_HEADER_SIZE + tws1.storedSize(this.size);
|
|
197
|
+
}
|
|
198
|
+
/** The bytes it adds to a reserve body (section 28.17): its JSON object and a separator. */
|
|
199
|
+
get request() {
|
|
200
|
+
if (!this.reqBytes)
|
|
201
|
+
this.reqBytes = new TextEncoder().encode(JSON.stringify(this.reserveJSON())).length + 2;
|
|
202
|
+
return this.reqBytes;
|
|
203
|
+
}
|
|
188
204
|
reserveJSON() {
|
|
189
205
|
const o = { key: this.key, size: this.size, sha256: this.sha256 };
|
|
190
206
|
if (this.contentType)
|
|
@@ -199,6 +215,11 @@ class Item {
|
|
|
199
215
|
}
|
|
200
216
|
}
|
|
201
217
|
exports.Item = Item;
|
|
218
|
+
/** A pack's states before its first signature: its bytes can't be on the storage network yet. */
|
|
219
|
+
const UNSIGNED = new Set(['new', 'reserved', 'built', 'conflict', 'bad']);
|
|
220
|
+
const itemStored = (it) => it.stored;
|
|
221
|
+
const itemKey = (it) => it.key;
|
|
222
|
+
const itemRequest = (it) => it.request;
|
|
202
223
|
/** One pack's state, kept in its store (a run's journal, or memory). */
|
|
203
224
|
class Pack {
|
|
204
225
|
bucket;
|
|
@@ -222,6 +243,18 @@ class Pack {
|
|
|
222
243
|
waited = 0;
|
|
223
244
|
throttled = false;
|
|
224
245
|
lastError = '';
|
|
246
|
+
/** The reason for a pack under the floor (section 28.9), sent with its reserve. */
|
|
247
|
+
underFloor;
|
|
248
|
+
/** Its bytes counted against MEMORY_CAP while in flight (putObject's packs). */
|
|
249
|
+
inflight = 0;
|
|
250
|
+
/** When its upload ended (in memory only), whether that answer was lost, and the checking waits since (28.18). */
|
|
251
|
+
sentAt;
|
|
252
|
+
sentUnknown = false;
|
|
253
|
+
verifyWaited = 0;
|
|
254
|
+
/** A run's packs give their files back to its planner instead of renewing under the floor (28.5). */
|
|
255
|
+
giveback;
|
|
256
|
+
/** Resumed, unsigned and under the floor (28.22 point 5): its idempotency key is asked first. */
|
|
257
|
+
dissolve = false;
|
|
225
258
|
constructor(bucket, items, store, id) {
|
|
226
259
|
this.bucket = bucket;
|
|
227
260
|
this.items = items;
|
|
@@ -399,11 +432,12 @@ class DirectUploads {
|
|
|
399
432
|
sizes = {
|
|
400
433
|
...engine_js_1.PART_RULES,
|
|
401
434
|
packObjectLimit: engine_js_1.PACK_OBJECT_LIMIT,
|
|
402
|
-
packTarget: engine_js_1.PACK_TARGET_BYTES,
|
|
403
|
-
packMinTarget: engine_js_1.PACK_MIN_TARGET,
|
|
404
|
-
packMaxObjects: engine_js_1.PACK_MAX_OBJECTS,
|
|
405
435
|
streamConcurrency: engine_js_1.STREAM_CONCURRENCY,
|
|
406
436
|
};
|
|
437
|
+
/** The pack sizes and timings (section 28): built in, then the server's from each reservation. Tests set them. */
|
|
438
|
+
limits = { ...packing_js_1.DEFAULT_LIMITS };
|
|
439
|
+
/** The timing rule's monotonic clock in milliseconds (tests replace it). */
|
|
440
|
+
clock = () => performance.now();
|
|
407
441
|
/**
|
|
408
442
|
* The measured upload speed to the storage network: shrinks packs and new uploads' parts on a slow link (section
|
|
409
443
|
* 22.7). Tests set it with speed.set.
|
|
@@ -414,6 +448,14 @@ class DirectUploads {
|
|
|
414
448
|
* slower, twice its time at that speed (section 22.7). Tests shorten it.
|
|
415
449
|
*/
|
|
416
450
|
putTimeout = { minMs: 30000, bytesPerSecond: 65536 };
|
|
451
|
+
/**
|
|
452
|
+
* A pack call's timeout (29.15): the server's time for `objects` objects, scaled, plus sending `body` (none for a
|
|
453
|
+
* commit) at the speed measured so far; never under `floorMs`.
|
|
454
|
+
*/
|
|
455
|
+
callTimeoutMs(objects, perObjectMs, floorMs, body) {
|
|
456
|
+
const n = body === undefined ? 0 : new TextEncoder().encode(JSON.stringify(body)).length;
|
|
457
|
+
return (0, engine_js_1.scaledTimeoutMs)(objects, perObjectMs, floorMs, (0, engine_js_1.bodyTransferMs)(n, this.speed.bytesPerSecond, this.putTimeout.bytesPerSecond));
|
|
458
|
+
}
|
|
417
459
|
/** How long a stopping step waits for its tasks, and a cleanup call may take (ms). */
|
|
418
460
|
cleanupMs = exports.CLEANUP_MS;
|
|
419
461
|
digests = exports.webDigests;
|
|
@@ -422,8 +464,17 @@ class DirectUploads {
|
|
|
422
464
|
host;
|
|
423
465
|
/** Where warnings go: a run removed by cleanUploads, a large state directory. */
|
|
424
466
|
warn = (m) => console.warn(m);
|
|
467
|
+
/** Where a line worth knowing but not a warning goes (a purged pack's commit, 29.15); tests replace it. */
|
|
468
|
+
info = (m) => console.info(m);
|
|
425
469
|
accountId;
|
|
426
|
-
|
|
470
|
+
/** putObject's waiting files, per bucket, with their running totals (section 28.17). */
|
|
471
|
+
waiting = new Map();
|
|
472
|
+
/** When the timing rule next needs a decision, if it does. */
|
|
473
|
+
nextDecision;
|
|
474
|
+
/** putObject's pack bytes in flight, against MEMORY_CAP. */
|
|
475
|
+
inflight = 0;
|
|
476
|
+
/** putObject's packs in flight and when each is expected back (section 28.25). */
|
|
477
|
+
inFlight = new packing_js_1.InFlight();
|
|
427
478
|
/** Wake-ups of putObject calls waiting for a slot: resolved when one comes free. */
|
|
428
479
|
slotWaiters = [];
|
|
429
480
|
memory = new PackStore();
|
|
@@ -501,10 +552,6 @@ class DirectUploads {
|
|
|
501
552
|
packObjectLimit() {
|
|
502
553
|
return (0, engine_js_1.packObjectLimit)(this.speed.requestBytes(), this.sizes.packObjectLimit, this.sizes.minBytes);
|
|
503
554
|
}
|
|
504
|
-
/** The next pack's target size, at the speed measured so far. */
|
|
505
|
-
packTarget() {
|
|
506
|
-
return (0, engine_js_1.packTarget)(this.speed.requestBytes(), this.sizes.packTarget, this.sizes.packMinTarget);
|
|
507
|
-
}
|
|
508
555
|
// --- shared plumbing ----------------------------------------------------------
|
|
509
556
|
/** Where runs keep their state, when it's on disk. */
|
|
510
557
|
get runHost() {
|
|
@@ -592,7 +639,9 @@ class DirectUploads {
|
|
|
592
639
|
*/
|
|
593
640
|
async drive(p, signal) {
|
|
594
641
|
try {
|
|
595
|
-
|
|
642
|
+
if (p.dissolve)
|
|
643
|
+
await this.dissolvePack(p, signal);
|
|
644
|
+
while (p.state !== 'committed' && p.state !== 'failed' && p.state !== 'returned') {
|
|
596
645
|
signal?.throwIfAborted();
|
|
597
646
|
switch (p.state) {
|
|
598
647
|
case 'new':
|
|
@@ -645,9 +694,11 @@ class DirectUploads {
|
|
|
645
694
|
async reserve(p, signal) {
|
|
646
695
|
await p.store.savePack(p); // the idempotency key, before the call
|
|
647
696
|
const body = { idempotency_key: p.idem, objects: p.items.map((it) => it.reserveJSON()) };
|
|
697
|
+
if (p.underFloor)
|
|
698
|
+
body.under_floor = p.underFloor;
|
|
648
699
|
let res;
|
|
649
700
|
try {
|
|
650
|
-
res = (await this.control(`POST`, `${bucketPath(p.bucket)}/packs`, body, p, signal)).data;
|
|
701
|
+
res = (await this.control(`POST`, `${bucketPath(p.bucket)}/packs`, body, p, signal, this.callTimeoutMs(p.items.length, engine_js_1.RESERVE_MS_PER_OBJECT, this.client.timeout, body))).data;
|
|
651
702
|
}
|
|
652
703
|
catch (err) {
|
|
653
704
|
if (err instanceof errors_js_1.APIError && err.code === 'reservation_abandoned') {
|
|
@@ -665,6 +716,9 @@ class DirectUploads {
|
|
|
665
716
|
throw err;
|
|
666
717
|
}
|
|
667
718
|
await this.checkKey(res.account_key);
|
|
719
|
+
const limits = (0, packing_js_1.limitsFromJSON)(res.limits);
|
|
720
|
+
if (limits)
|
|
721
|
+
this.limits = limits;
|
|
668
722
|
const ids = new Map((res.objects ?? []).map((o) => [str(o.key), str(o.object_id)]));
|
|
669
723
|
for (const it of p.items)
|
|
670
724
|
it.objectId = ids.get(it.key) ?? '';
|
|
@@ -680,6 +734,9 @@ class DirectUploads {
|
|
|
680
734
|
// The reservation names files that changed: the rest go under a new one.
|
|
681
735
|
const gone = new Set(built.changed.map(([it]) => it));
|
|
682
736
|
p.items = p.items.filter((it) => !gone.has(it));
|
|
737
|
+
// A run's other files go back to its planner, so they never go as a smaller pack (section 28.5).
|
|
738
|
+
if (p.giveback && p.items.length)
|
|
739
|
+
return this.giveBack(p);
|
|
683
740
|
p.state = p.items.length ? 'conflict' : 'failed';
|
|
684
741
|
p.rebuilds--; // not the pack's fault
|
|
685
742
|
await p.store.savePack(p);
|
|
@@ -753,7 +810,7 @@ class DirectUploads {
|
|
|
753
810
|
async sign(p, signal) {
|
|
754
811
|
let signed;
|
|
755
812
|
try {
|
|
756
|
-
signed = (await this.control(`POST`, `${bucketPath(p.bucket)}/packs/${p.serverId}/sign`, p.signBody, p, signal)).data;
|
|
813
|
+
signed = (await this.control(`POST`, `${bucketPath(p.bucket)}/packs/${p.serverId}/sign`, p.signBody, p, signal, this.callTimeoutMs(p.items.length, engine_js_1.SIGN_MS_PER_OBJECT, this.client.timeout, p.signBody))).data;
|
|
757
814
|
}
|
|
758
815
|
catch (err) {
|
|
759
816
|
if (err instanceof errors_js_1.APIError && err.code === 'reservation_abandoned') {
|
|
@@ -783,6 +840,7 @@ class DirectUploads {
|
|
|
783
840
|
return;
|
|
784
841
|
}
|
|
785
842
|
p.state = 'uploading';
|
|
843
|
+
this.inFlight.uploading(p.id, this.clock());
|
|
786
844
|
await p.store.savePack(p);
|
|
787
845
|
let err;
|
|
788
846
|
this.budget.onRequest();
|
|
@@ -796,7 +854,14 @@ class DirectUploads {
|
|
|
796
854
|
throw e;
|
|
797
855
|
err = e;
|
|
798
856
|
}
|
|
799
|
-
|
|
857
|
+
const outcome = (0, engine_js_1.storageOutcome)(err);
|
|
858
|
+
if (outcome === 'sent' || outcome === 'unknown') {
|
|
859
|
+
p.sentAt = this.clock();
|
|
860
|
+
p.sentUnknown = outcome === 'unknown';
|
|
861
|
+
p.verifyWaited = 0;
|
|
862
|
+
this.inFlight.sent(p.id, p.sentAt);
|
|
863
|
+
}
|
|
864
|
+
switch (outcome) {
|
|
800
865
|
case 'sent':
|
|
801
866
|
p.state = 'sent';
|
|
802
867
|
break;
|
|
@@ -836,7 +901,7 @@ class DirectUploads {
|
|
|
836
901
|
let status;
|
|
837
902
|
let res;
|
|
838
903
|
try {
|
|
839
|
-
({ status, data: res } = await this.control(`POST`, `${bucketPath(p.bucket)}/packs/${p.serverId}/commit`, undefined, p, signal,
|
|
904
|
+
({ status, data: res } = await this.control(`POST`, `${bucketPath(p.bucket)}/packs/${p.serverId}/commit`, undefined, p, signal, this.callTimeoutMs(p.items.length, engine_js_1.COMMIT_MS_PER_OBJECT, engine_js_1.COMMIT_TIMEOUT_MS)));
|
|
840
905
|
}
|
|
841
906
|
catch (err) {
|
|
842
907
|
if (!(err instanceof errors_js_1.APIError))
|
|
@@ -855,13 +920,34 @@ class DirectUploads {
|
|
|
855
920
|
return;
|
|
856
921
|
}
|
|
857
922
|
if (status === 202) {
|
|
858
|
-
|
|
859
|
-
|
|
923
|
+
// Not there yet (section 28.18): asked again after 250 ms, 500 ms, 1 s and 2 s, then every 2 s; the same bytes are
|
|
924
|
+
// sent again only 30 s after an upload that answered success, or 5 s after one whose answer was lost (or a pack
|
|
925
|
+
// resumed mid-upload). The time since is the larger of the clock's and the waits'. The waits count against
|
|
926
|
+
// uploadWait, but a pack always gets its first full cycle: a pack the network never shows ends after one sending
|
|
927
|
+
// again, kept for resumeUploads.
|
|
928
|
+
const now = this.clock();
|
|
929
|
+
if (p.sentAt === undefined) {
|
|
930
|
+
p.sentAt = now;
|
|
931
|
+
p.sentUnknown = unknown;
|
|
932
|
+
p.verifyWaited = 0;
|
|
933
|
+
}
|
|
934
|
+
const limit = p.sentUnknown ? engine_js_1.RESEND_AFTER_LOST_MS : engine_js_1.RESEND_AFTER_SENT_MS;
|
|
935
|
+
const since = Math.max(now - p.sentAt, p.verifyWaited);
|
|
936
|
+
if (since >= limit) {
|
|
860
937
|
p.state = 'signed'; // not there: send the same bytes again
|
|
861
938
|
p.verifies = 0;
|
|
939
|
+
p.sentAt = undefined;
|
|
862
940
|
}
|
|
863
941
|
else {
|
|
864
|
-
|
|
942
|
+
const delay = Math.min((0, engine_js_1.verifyDelayMs)(p.verifies), limit - since); // the last look comes as the bytes may go again
|
|
943
|
+
p.verifies++;
|
|
944
|
+
const bound = Math.max(this.client.uploadWait, limit);
|
|
945
|
+
if (p.waited + delay > bound) {
|
|
946
|
+
throw new errors_js_1.StorageUnavailableError(`storage didn't show the pack within ${Math.round(bound / 1000)} s of waiting`);
|
|
947
|
+
}
|
|
948
|
+
p.waited += delay;
|
|
949
|
+
p.verifyWaited += delay;
|
|
950
|
+
await this.client.sleep(delay, signal);
|
|
865
951
|
}
|
|
866
952
|
await p.store.savePack(p);
|
|
867
953
|
return;
|
|
@@ -871,11 +957,19 @@ class DirectUploads {
|
|
|
871
957
|
await p.store.dropBytes(p);
|
|
872
958
|
await p.store.savePack(p);
|
|
873
959
|
}
|
|
874
|
-
/**
|
|
960
|
+
/**
|
|
961
|
+
* A new reservation and fresh keys for the pack's items (section 17.3). A run's pack under the floor gives its files
|
|
962
|
+
* back to the planner instead (section 28.5); the old reservation is released (section 28.23).
|
|
963
|
+
*/
|
|
875
964
|
async renew(p) {
|
|
876
965
|
p.rebuilds++;
|
|
877
966
|
if (p.rebuilds > engine_js_1.MAX_REBUILDS)
|
|
878
967
|
throw new Stop(new errors_js_1.TeepinError(`the pack was rebuilt ${engine_js_1.MAX_REBUILDS} times: ${p.lastError || p.state}`));
|
|
968
|
+
const stored = p.items.reduce((n, it) => n + it.stored, 0);
|
|
969
|
+
if (p.giveback && stored < this.limits.floor)
|
|
970
|
+
return this.giveBack(p);
|
|
971
|
+
if (p.serverId)
|
|
972
|
+
await this.releaseReservation(p);
|
|
879
973
|
p.lastError = p.state;
|
|
880
974
|
await p.store.dropBytes(p);
|
|
881
975
|
p.idem = (0, common_js_1.uuid7)();
|
|
@@ -889,9 +983,82 @@ class DirectUploads {
|
|
|
889
983
|
p.state = 'new';
|
|
890
984
|
await p.store.savePack(p);
|
|
891
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* A resumed pack that was never signed and is under the floor: reserve is asked with its own idempotency key (and its
|
|
988
|
+
* files as journaled), so a pack an earlier run already committed answers pack_committed and is committed for its
|
|
989
|
+
* stored result. Any other answer releases what it reserved, and its files go back to the planner (28.22, point 5).
|
|
990
|
+
*/
|
|
991
|
+
async dissolvePack(p, signal) {
|
|
992
|
+
p.dissolve = false;
|
|
993
|
+
p.underFloor = 'leftover'; // not part of the idempotency digest
|
|
994
|
+
try {
|
|
995
|
+
await this.reserve(p, signal);
|
|
996
|
+
}
|
|
997
|
+
catch (err) {
|
|
998
|
+
if (signal?.aborted || !(err instanceof errors_js_1.APIError))
|
|
999
|
+
throw err;
|
|
1000
|
+
// e.g. pack_too_small from a server in enforce: nothing was committed under this key
|
|
1001
|
+
}
|
|
1002
|
+
if (p.state === 'committing')
|
|
1003
|
+
return;
|
|
1004
|
+
await this.giveBack(p);
|
|
1005
|
+
}
|
|
1006
|
+
/** Hands a run's pack's remaining files back to its planner (section 28.5); the pack ends as returned. */
|
|
1007
|
+
async giveBack(p) {
|
|
1008
|
+
if (p.serverId)
|
|
1009
|
+
await this.releaseReservation(p);
|
|
1010
|
+
await p.store.dropBytes(p);
|
|
1011
|
+
p.state = 'returned';
|
|
1012
|
+
await p.store.savePack(p);
|
|
1013
|
+
const items = p.items.filter((it) => !it.result && it.error === undefined);
|
|
1014
|
+
for (const it of items) {
|
|
1015
|
+
it.objectId = '';
|
|
1016
|
+
it.rebuilds = Math.max(it.rebuilds, p.rebuilds);
|
|
1017
|
+
}
|
|
1018
|
+
p.giveback(items);
|
|
1019
|
+
}
|
|
1020
|
+
/** Frees a given-up reservation at once (section 28.23), best effort: without the route it expires as before. */
|
|
1021
|
+
async releaseReservation(p) {
|
|
1022
|
+
try {
|
|
1023
|
+
const { response } = await this.client.send({ method: 'POST', path: `${bucketPath(p.bucket)}/packs/${p.serverId}/release`, mode: 'safe' });
|
|
1024
|
+
await response.arrayBuffer().catch(() => undefined);
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
// a deployment without the route, or any failure: the reservation expires on its own
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
892
1030
|
async deliver(p, res) {
|
|
893
1031
|
const when = new Date(str(res.committed_at) || NaN);
|
|
894
1032
|
const backend = str(res.backend);
|
|
1033
|
+
if (res.purged === true) {
|
|
1034
|
+
// Committed, then every object in it deleted and the pack's row purged (TEEPIN-STORAGE-DESIGN.md 29.15): the answer
|
|
1035
|
+
// has no per-object results, but each file was saved. Never uploaded again.
|
|
1036
|
+
this.info(`teepin: pack ${p.serverId} was committed at ${str(res.committed_at)}; its objects have since been deleted`);
|
|
1037
|
+
const saved = p.items
|
|
1038
|
+
.filter((it) => !it.result && it.error === undefined)
|
|
1039
|
+
.map((it) => [
|
|
1040
|
+
it,
|
|
1041
|
+
{
|
|
1042
|
+
id: it.objectId,
|
|
1043
|
+
key: it.key,
|
|
1044
|
+
size: it.size,
|
|
1045
|
+
contentType: it.contentType || undefined,
|
|
1046
|
+
metadata: it.metadata,
|
|
1047
|
+
checksumSha256: it.sha256,
|
|
1048
|
+
etag: it.sha256,
|
|
1049
|
+
partCount: 0,
|
|
1050
|
+
status: 'deleted',
|
|
1051
|
+
backend: '',
|
|
1052
|
+
createdAt: when,
|
|
1053
|
+
updatedAt: when,
|
|
1054
|
+
uploadedAt: when,
|
|
1055
|
+
},
|
|
1056
|
+
undefined,
|
|
1057
|
+
true,
|
|
1058
|
+
]);
|
|
1059
|
+
await this.done(p.store, saved);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
895
1062
|
const objects = res.objects ?? [];
|
|
896
1063
|
const byId = new Map(objects.map((o) => [str(o.object_id), o]));
|
|
897
1064
|
const byKey = new Map(objects.map((o) => [str(o.key), o]));
|
|
@@ -941,17 +1108,26 @@ class DirectUploads {
|
|
|
941
1108
|
const sha = await this.digests.sha256(data);
|
|
942
1109
|
const it = new Item(key, data.length, sha, async () => data, opts.contentType, opts.metadata, Boolean(opts.ifNoneMatch), normalizeIfMatch(opts.ifMatch));
|
|
943
1110
|
it.done = new common_js_1.Deferred();
|
|
944
|
-
this.
|
|
945
|
-
|
|
1111
|
+
let arrivals = this.waiting.get(bucket);
|
|
1112
|
+
if (!arrivals) {
|
|
1113
|
+
arrivals = new packing_js_1.Arrivals();
|
|
1114
|
+
this.waiting.set(bucket, arrivals);
|
|
1115
|
+
}
|
|
1116
|
+
arrivals.add(it, it.key, it.stored, it.request, this.clock());
|
|
946
1117
|
const wait = this.client.uploadWait;
|
|
947
|
-
|
|
1118
|
+
// The observe time (28.25) is the timing rule's own wait, not one for a slot: it never counts against uploadWait (a
|
|
1119
|
+
// lone save with uploadWait 0 still goes).
|
|
1120
|
+
const deadline = Date.now() + wait + this.limits.observeMs;
|
|
948
1121
|
const leave = () => {
|
|
949
|
-
const
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1122
|
+
const a = this.waiting.get(bucket);
|
|
1123
|
+
if (!a || it.picked)
|
|
1124
|
+
return false;
|
|
1125
|
+
a.remove([it]);
|
|
1126
|
+
if (!a.size)
|
|
1127
|
+
this.waiting.delete(bucket);
|
|
1128
|
+
if (!this.waiting.size)
|
|
1129
|
+
this.limit.setPriority(false); // no save is left to hold a run up
|
|
1130
|
+
return true;
|
|
955
1131
|
};
|
|
956
1132
|
try {
|
|
957
1133
|
while (!it.picked) {
|
|
@@ -967,8 +1143,11 @@ class DirectUploads {
|
|
|
967
1143
|
break;
|
|
968
1144
|
let timer;
|
|
969
1145
|
try {
|
|
970
|
-
// A timer past 2^31 - 1 ms fires at once in Node and browsers, so a very long uploadWait waits in slices
|
|
971
|
-
|
|
1146
|
+
// A timer past 2^31 - 1 ms fires at once in Node and browsers, so a very long uploadWait waits in slices; the
|
|
1147
|
+
// timing rule's next decision comes sooner.
|
|
1148
|
+
let slice = Math.min(left, MAX_TIMER_MS);
|
|
1149
|
+
if (this.nextDecision !== undefined)
|
|
1150
|
+
slice = Math.min(slice, Math.max(1, this.nextDecision - this.clock()));
|
|
972
1151
|
await (0, common_js_1.abortable)(Promise.race([woken.promise, new Promise((r) => (timer = setTimeout(r, slice)))]), signal);
|
|
973
1152
|
}
|
|
974
1153
|
finally {
|
|
@@ -989,21 +1168,67 @@ class DirectUploads {
|
|
|
989
1168
|
throw new errors_js_1.TeepinError(`${key}: no result`);
|
|
990
1169
|
return it.result;
|
|
991
1170
|
}
|
|
992
|
-
/**
|
|
1171
|
+
/**
|
|
1172
|
+
* Sends the packs section 28's timing rule allows, oldest bucket first, each when a slot is free and the memory cap
|
|
1173
|
+
* allows, and notes when the rule next needs a decision. While a pack would go but has no slot, a run's next pack
|
|
1174
|
+
* waits for it (priority); a pack that is only gathering holds no run up. Replaced in tests.
|
|
1175
|
+
*/
|
|
993
1176
|
dispatch() {
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
this.limit.setPriority(this.queue.length > 0);
|
|
1001
|
-
for (const it of batch) {
|
|
1177
|
+
for (;;) {
|
|
1178
|
+
const chosen = this.choose();
|
|
1179
|
+
if (!chosen)
|
|
1180
|
+
return;
|
|
1181
|
+
const [bucket, plan, reason] = chosen;
|
|
1182
|
+
for (const it of plan.pack) {
|
|
1002
1183
|
it.picked = true;
|
|
1003
|
-
it.wake?.resolve(); // its caller may be waiting
|
|
1184
|
+
it.wake?.resolve(); // its caller may be waiting: its file has a pack now
|
|
1004
1185
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1186
|
+
const p = new Pack(bucket, plan.pack, this.memory);
|
|
1187
|
+
p.underFloor = reason;
|
|
1188
|
+
p.inflight = plan.stored;
|
|
1189
|
+
this.inFlight.add(p.id, bucket, plan.stored);
|
|
1190
|
+
void this.runShared(p);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
/** The next pack to send and its reason (a slot taken for it), or undefined; sets the next decision and priority. */
|
|
1194
|
+
choose() {
|
|
1195
|
+
const now = this.clock();
|
|
1196
|
+
const speed = this.speed.bytesPerSecond;
|
|
1197
|
+
const requestBytes = this.speed.requestBytes();
|
|
1198
|
+
const [limits, slow] = (0, packing_js_1.atSpeed)(this.limits, requestBytes);
|
|
1199
|
+
const [choices, next] = (0, packing_js_1.schedule)(this.waiting, now, limits, (a, tailCap) => (0, packing_js_1.planPack)(a.items(), itemStored, itemKey, itemRequest, limits, tailCap !== undefined, tailCap), (bucket, by) => this.inFlight.coming(bucket, by, now, speed, limits.observeMs, (t) => this.waiting.get(bucket)?.arrivedSince(t) ?? 0), this.limit.runsWaiting(), slow ? 0 : (0, packing_js_1.absorbCap)(this.limits, requestBytes));
|
|
1200
|
+
this.nextDecision = next;
|
|
1201
|
+
let blocked = false;
|
|
1202
|
+
for (const c of choices) {
|
|
1203
|
+
const p = c.plan;
|
|
1204
|
+
if (this.inflight > 0 && this.inflight + p.stored > packing_js_1.MEMORY_CAP) {
|
|
1205
|
+
blocked = true;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (!this.limit.tryAcquire()) {
|
|
1209
|
+
blocked = true;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
const a = this.waiting.get(c.bucket);
|
|
1213
|
+
a.remove(p.pack);
|
|
1214
|
+
if (!a.size)
|
|
1215
|
+
this.waiting.delete(c.bucket);
|
|
1216
|
+
this.inflight += p.stored;
|
|
1217
|
+
this.limit.setPriority(blocked);
|
|
1218
|
+
return [c.bucket, p, this.reasonFor(p, c.decision.reason, slow)];
|
|
1219
|
+
}
|
|
1220
|
+
this.limit.setPriority(blocked);
|
|
1221
|
+
return undefined;
|
|
1222
|
+
}
|
|
1223
|
+
/** A pack's reason for being under the floor (section 28.9), or undefined at or over it. */
|
|
1224
|
+
reasonFor(p, reason, slow) {
|
|
1225
|
+
if (p.stored >= this.limits.floor)
|
|
1226
|
+
return undefined;
|
|
1227
|
+
if (slow)
|
|
1228
|
+
return 'slow_link';
|
|
1229
|
+
if (p.state === 'request_limited')
|
|
1230
|
+
return 'request_limit';
|
|
1231
|
+
return reason ?? 'leftover';
|
|
1007
1232
|
}
|
|
1008
1233
|
async runShared(p) {
|
|
1009
1234
|
let outcome = 'failed';
|
|
@@ -1017,8 +1242,13 @@ class DirectUploads {
|
|
|
1017
1242
|
it.error = err; // every caller gets its answer
|
|
1018
1243
|
}
|
|
1019
1244
|
finally {
|
|
1245
|
+
// Out of the packs in flight, into its grace time (28.25), before its slot is released. Its callers were woken as
|
|
1246
|
+
// each item was delivered: one that saved again meanwhile counted twice until here, which can only hold a pack, and
|
|
1247
|
+
// the release below has it decided again.
|
|
1248
|
+
this.inFlight.remove(p.id, this.clock(), p.state === 'committed');
|
|
1020
1249
|
for (const it of p.items)
|
|
1021
1250
|
it.done?.resolve();
|
|
1251
|
+
this.inflight -= p.inflight;
|
|
1022
1252
|
this.release(outcome);
|
|
1023
1253
|
}
|
|
1024
1254
|
}
|
|
@@ -1239,6 +1469,7 @@ class DirectUploads {
|
|
|
1239
1469
|
}
|
|
1240
1470
|
async sendRun(state, onProgress, partSize, signal) {
|
|
1241
1471
|
const { bucket, store, place, items } = state;
|
|
1472
|
+
items.forEach((it, i) => (it.order = i));
|
|
1242
1473
|
const lim = this.packObjectLimit();
|
|
1243
1474
|
// A file whose upload in parts a crash left open goes on in parts, whatever the speed now (section 22.7).
|
|
1244
1475
|
const inParts = new Set(store.journal
|
|
@@ -1247,7 +1478,7 @@ class DirectUploads {
|
|
|
1247
1478
|
.map((r) => str(r.file_id)));
|
|
1248
1479
|
const large = items.filter((it) => it.size > lim || (it.size > 0 && it.fileId !== undefined && inParts.has(it.fileId)));
|
|
1249
1480
|
const chosen = new Set(large);
|
|
1250
|
-
|
|
1481
|
+
let small = items.filter((it) => it.size > 0 && !chosen.has(it));
|
|
1251
1482
|
// The small files are packed as slots come free, each pack at the speed measured by then (section 22.7).
|
|
1252
1483
|
const packs = [...state.packs];
|
|
1253
1484
|
let total = 0;
|
|
@@ -1299,32 +1530,56 @@ class DirectUploads {
|
|
|
1299
1530
|
await this.abandon(store, place); // the last task of an interrupted run releases it
|
|
1300
1531
|
}
|
|
1301
1532
|
};
|
|
1302
|
-
|
|
1533
|
+
// Files a pack gives back (a changed file's pack, a renewal under the floor) are planned again with the rest, their
|
|
1534
|
+
// place in the run kept (section 28.5).
|
|
1535
|
+
let back = [];
|
|
1536
|
+
let tasks = [];
|
|
1303
1537
|
try {
|
|
1304
1538
|
const start = (p) => {
|
|
1539
|
+
p.giveback = (returned) => back.push(...returned);
|
|
1305
1540
|
closer.started();
|
|
1306
1541
|
tasks.push(go(p).catch(failed));
|
|
1307
1542
|
};
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1543
|
+
// A first round dissolves the resumed packs under the floor (section 28.22, point 5), so their files are planned
|
|
1544
|
+
// with the queued ones; the other resumed packs and the queued files wait for it.
|
|
1545
|
+
const dissolving = packs.filter((p) => p.dissolve);
|
|
1546
|
+
for (const group of [dissolving, packs.filter((p) => !p.dissolve)]) {
|
|
1547
|
+
for (const p of group) {
|
|
1548
|
+
await this.limit.acquire(ctl.signal);
|
|
1549
|
+
if (failure) {
|
|
1550
|
+
this.release('failed');
|
|
1551
|
+
throw failure.err;
|
|
1552
|
+
}
|
|
1553
|
+
start(p);
|
|
1313
1554
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1555
|
+
if (group === dissolving && dissolving.length) {
|
|
1556
|
+
await (0, common_js_1.abortable)(Promise.all(tasks), ctl.signal);
|
|
1557
|
+
if (failure)
|
|
1558
|
+
throw failure.err;
|
|
1559
|
+
tasks = [];
|
|
1560
|
+
small = [...back, ...small].sort((a, b) => a.order - b.order);
|
|
1561
|
+
back = [];
|
|
1321
1562
|
}
|
|
1322
|
-
let chunk;
|
|
1323
|
-
[chunk, rest] = (0, engine_js_1.nextPack)(rest, (i) => i.size, (i) => i.key, this.packTarget(), this.sizes.packMaxObjects);
|
|
1324
|
-
const p = new Pack(bucket, chunk, store);
|
|
1325
|
-
packs.push(p);
|
|
1326
|
-
start(p);
|
|
1327
1563
|
}
|
|
1564
|
+
// A pack for each slot that comes free, planned at the speed measured by then, the run's tail joining its last pack.
|
|
1565
|
+
const planSmall = async (files) => {
|
|
1566
|
+
for (let rest = files; rest.length;) {
|
|
1567
|
+
await this.limit.acquire(ctl.signal);
|
|
1568
|
+
if (failure) {
|
|
1569
|
+
this.release('failed');
|
|
1570
|
+
throw failure.err;
|
|
1571
|
+
}
|
|
1572
|
+
const [limits, slow] = (0, packing_js_1.atSpeed)(this.limits, this.speed.requestBytes());
|
|
1573
|
+
const plan = (0, packing_js_1.planPack)(rest, itemStored, itemKey, itemRequest, limits, true);
|
|
1574
|
+
rest = plan.rest;
|
|
1575
|
+
const p = new Pack(bucket, plan.pack, store);
|
|
1576
|
+
p.underFloor = this.reasonFor(plan, 'leftover', slow);
|
|
1577
|
+
p.rebuilds = Math.max(0, ...plan.pack.map((it) => it.rebuilds));
|
|
1578
|
+
packs.push(p);
|
|
1579
|
+
start(p);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
await planSmall(small);
|
|
1328
1583
|
for (const it of large) {
|
|
1329
1584
|
if (stop.length) {
|
|
1330
1585
|
await this.done(store, [[it, undefined, stop[0], false]]);
|
|
@@ -1350,10 +1605,19 @@ class DirectUploads {
|
|
|
1350
1605
|
throw err;
|
|
1351
1606
|
}
|
|
1352
1607
|
}
|
|
1353
|
-
// A task that fails stops the run at once (its controller aborts), as joining it first would.
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1608
|
+
// A task that fails stops the run at once (its controller aborts), as joining it first would. What packs gave back
|
|
1609
|
+
// is planned again with its place in the run kept, until none gives any back.
|
|
1610
|
+
for (;;) {
|
|
1611
|
+
await (0, common_js_1.abortable)(Promise.all(tasks), ctl.signal);
|
|
1612
|
+
if (failure)
|
|
1613
|
+
throw failure.err;
|
|
1614
|
+
const again = back.sort((a, b) => a.order - b.order);
|
|
1615
|
+
back = [];
|
|
1616
|
+
if (!again.length)
|
|
1617
|
+
break;
|
|
1618
|
+
tasks = [];
|
|
1619
|
+
await planSmall(again);
|
|
1620
|
+
}
|
|
1357
1621
|
}
|
|
1358
1622
|
catch (err) {
|
|
1359
1623
|
// Interrupted, or failed: every pack still under way is stopped and waited for (up to the cap); then the run's
|
|
@@ -1538,7 +1802,7 @@ class DirectUploads {
|
|
|
1538
1802
|
const packs = [];
|
|
1539
1803
|
for (const r of j.rows('packs')) {
|
|
1540
1804
|
const pid = str(r._id);
|
|
1541
|
-
if (r.state === 'committed' || r.state === 'failed')
|
|
1805
|
+
if (r.state === 'committed' || r.state === 'failed' || r.state === 'returned')
|
|
1542
1806
|
continue;
|
|
1543
1807
|
const its = byPack.get(pid);
|
|
1544
1808
|
byPack.delete(pid);
|
|
@@ -1557,10 +1821,18 @@ class DirectUploads {
|
|
|
1557
1821
|
p.attempts = int(r.attempts);
|
|
1558
1822
|
if (p.state === 'uploading')
|
|
1559
1823
|
p.state = 'unknown'; // the PUT may have landed: commit first
|
|
1824
|
+
// Not yet signed and under the floor (section 28.22, point 5): its files are planned again, once its idempotency
|
|
1825
|
+
// key has shown it wasn't committed (dissolvePack).
|
|
1826
|
+
p.dissolve = UNSIGNED.has(p.state) && its.reduce((n, it) => n + it.stored, 0) < this.limits.floor;
|
|
1560
1827
|
packs.push(p);
|
|
1561
1828
|
}
|
|
1562
1829
|
for (const its of byPack.values())
|
|
1563
1830
|
queued.push(...its); // packed into a pack that's gone: queue them again
|
|
1831
|
+
// Dissolving gains nothing when there is nothing to merge with: a lone small pack would be planned again as the same
|
|
1832
|
+
// leftover, and carrying on under its own idempotency key keeps a restored older journal from sending files twice.
|
|
1833
|
+
if (packs.filter((p) => p.dissolve).length + (queued.length ? 1 : 0) < 2)
|
|
1834
|
+
for (const p of packs)
|
|
1835
|
+
p.dissolve = false;
|
|
1564
1836
|
await this.sweepOrphans(place, j, packs);
|
|
1565
1837
|
return { state: { bucket, store, place, items: queued, packs, results }, changed };
|
|
1566
1838
|
}
|