@procedures/mongo 0.48.2 → 0.49.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 +35 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/storage/database-changes.d.ts +15 -2
- package/dist/storage/database-changes.d.ts.map +1 -1
- package/dist/storage/database-changes.js +30 -5
- package/dist/storage/database-changes.js.map +1 -1
- package/dist/storage/document-storage.d.ts +2 -1
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +66 -18
- package/dist/storage/document-storage.js.map +1 -1
- package/dist/storage/materializer.d.ts.map +1 -1
- package/dist/storage/materializer.js +6 -5
- package/dist/storage/materializer.js.map +1 -1
- package/dist/storage/pool-share.d.ts +24 -0
- package/dist/storage/pool-share.d.ts.map +1 -0
- package/dist/storage/pool-share.js +27 -0
- package/dist/storage/pool-share.js.map +1 -0
- package/dist/storage/split-events.d.ts +57 -0
- package/dist/storage/split-events.d.ts.map +1 -0
- package/dist/storage/split-events.js +94 -0
- package/dist/storage/split-events.js.map +1 -0
- package/dist/storage/storage-hints.d.ts +7 -0
- package/dist/storage/storage-hints.d.ts.map +1 -1
- package/dist/storage/storage-hints.js +41 -7
- package/dist/storage/storage-hints.js.map +1 -1
- package/package.json +6 -3
- package/src/storage/database-changes.ts +44 -7
- package/src/storage/document-storage.ts +102 -35
- package/src/storage/materializer.ts +6 -5
- package/src/storage/pool-share.ts +30 -0
- package/src/storage/split-events.ts +135 -0
- package/src/storage/storage-hints.ts +44 -7
|
@@ -62,6 +62,7 @@ import { BSON, ObjectId, type AnyBulkWriteOperation, type ChangeStream, type Cli
|
|
|
62
62
|
import { collectionChangeTopic, ms, optionalMs, realClock, Semaphore, writeLog, type Cancel, type Clock, type CollectionChangeEvent, type Duration, type LogFields, type Logger } from "@procedures/client";
|
|
63
63
|
import { DEFAULT_STORED_FORM, DOCUMENT_STORAGE_FIELDS, MongoDocumentStorage, TOMBSTONE_FIELD, storageWriteTarget, type StorageWriteTarget, type StoredForm } from "./document-storage.js";
|
|
64
64
|
import { scopePipeline } from "./pipeline-scope.js";
|
|
65
|
+
import { poolShare } from "./pool-share.js";
|
|
65
66
|
import type { StorageHintPublisher } from "./storage-hints.js";
|
|
66
67
|
|
|
67
68
|
/** The fields of a storage that a row never keeps: its sequence number and its tombstone flag (roadmap CLI-77). */
|
|
@@ -319,9 +320,7 @@ const MOST_PASSES = 32;
|
|
|
319
320
|
* a time, so two thirds of the pool stay free for the other operations of the process.
|
|
320
321
|
*/
|
|
321
322
|
function passLimit(db: Db): number {
|
|
322
|
-
|
|
323
|
-
const pool = (db.client as Db["client"] | undefined)?.options.maxPoolSize;
|
|
324
|
-
return pool ? Math.max(1, Math.min(MOST_PASSES, Math.floor(pool / 3))) : MOST_PASSES;
|
|
323
|
+
return poolShare(db, 3, MOST_PASSES);
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
/** The `_id` of a row in the result, from the stored `_id` of `partitionTargetId`. */
|
|
@@ -792,7 +791,8 @@ export class ViewMaterializer {
|
|
|
792
791
|
void this.track(this.onViewChange(change)).catch((error: unknown) => this.log("error", "A change of a view failed", { collection: this.viewsName, operationType: change["operationType"], error }, `a change of a view failed: ${String((error as Error)?.message ?? error)}`));
|
|
793
792
|
});
|
|
794
793
|
stream.on("error", (error: Error) => rewatch(`failed: ${error.message}`));
|
|
795
|
-
|
|
794
|
+
// The cursor of the driver tells its close before the error of a failed read. The close waits one turn, so the log gives the error (roadmap CLI-107)
|
|
795
|
+
stream.on("close", () => setImmediate(() => rewatch("closed")));
|
|
796
796
|
return openOnServer(stream);
|
|
797
797
|
}
|
|
798
798
|
|
|
@@ -1024,7 +1024,8 @@ export class ViewMaterializer {
|
|
|
1024
1024
|
if (watch.stream === stream) this.route(change, watch);
|
|
1025
1025
|
});
|
|
1026
1026
|
stream.on("error", (error: Error) => rewatch(`failed: ${error.message}`));
|
|
1027
|
-
|
|
1027
|
+
// The cursor of the driver tells its close before the error of a failed read. The close waits one turn, so the log gives the error (roadmap CLI-107)
|
|
1028
|
+
stream.on("close", () => setImmediate(() => rewatch("closed")));
|
|
1028
1029
|
return openOnServer(stream);
|
|
1029
1030
|
}
|
|
1030
1031
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Db } from "mongodb";
|
|
2
|
+
|
|
3
|
+
/** The pool size of the driver when the client does not tell its size (the default of `maxPoolSize`). */
|
|
4
|
+
export const DEFAULT_POOL_SIZE = 100;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The part of the pool for the change reads of one client: one twelfth, so 8 reads at the default pool.
|
|
8
|
+
* A burst of change reads waits on round trips over a network. So a share of the pool, not a fixed count, sets the limit.
|
|
9
|
+
*
|
|
10
|
+
* The objective: in a burst, the p95 of other queries of the pool stays within 1.5 round trips.
|
|
11
|
+
* The share of the pool controls that p95 only when the connections wait on the network.
|
|
12
|
+
* So the suite `read-bound` of `stream-cost.yml` gates the objective at a delay of 20 ms.
|
|
13
|
+
* There, 8 reads at one time gave 1.01 to 1.44 round trips (run 42105731536). 16 reads gave up to 1.86, and 4 reads gave a burst twice as long.
|
|
14
|
+
*
|
|
15
|
+
* At a short round trip, the work of the server sets the p95 of each client of the server.
|
|
16
|
+
* A limit for one pool only spreads that work. At 5 ms, 8 reads gave up to 3 round trips, and 4 reads gave 2.2 (run 42041953313).
|
|
17
|
+
* There the suite only reports, with the p95 over the quiet p95 of the same probe.
|
|
18
|
+
*/
|
|
19
|
+
export const CHANGE_READ_DIVISOR = 12;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This function gives the limit of operations of one kind at one time on a pool: the pool size over `divisor`,
|
|
23
|
+
* from 1 to `most`. Each operation uses one connection at a time. So the other operations of the process find a free
|
|
24
|
+
* connection in a burst, and a larger pool gives a larger limit.
|
|
25
|
+
*/
|
|
26
|
+
export function poolShare(db: Db, divisor: number, most: number = Number.POSITIVE_INFINITY): number {
|
|
27
|
+
// A fake database of a test has no client. A pool size of 0 has no limit in the driver: the share then uses the default size
|
|
28
|
+
const pool = (db.client as Db["client"] | undefined)?.options.maxPoolSize || DEFAULT_POOL_SIZE;
|
|
29
|
+
return Math.max(1, Math.min(most, Math.floor(pool / divisor)));
|
|
30
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The change events over the BSON limit of 16 MiB (roadmap CLI-107). An event holds the images of
|
|
3
|
+
* the document before and after its write. Thus a write of a small field can make an event over the
|
|
4
|
+
* limit. The server then fails the read of that event, and a stream that opens again at the same
|
|
5
|
+
* token fails again.
|
|
6
|
+
*
|
|
7
|
+
* - The stage `$changeStreamSplitLargeEvent` (MongoDB 7.0, and 6.0.9 or later) splits such an event
|
|
8
|
+
* into fragments. Each fragment has its own `_id` and the field `splitEvent: { fragment, of }`.
|
|
9
|
+
* The stage is the last stage of the pipeline.
|
|
10
|
+
* - `eventJoiner()` joins the fragments of one event into one event. The joined event has the `_id`
|
|
11
|
+
* of its last fragment, and no field `splitEvent`.
|
|
12
|
+
* - A stream that starts after a fragment gets the fragments after it. The joiner then tells that the
|
|
13
|
+
* first fragments are missing.
|
|
14
|
+
* - `sameTokenFaults()` counts the errors with one code at one resume token. A fault that stays at one
|
|
15
|
+
* event comes again at each open. A network error and an error that the driver resumes do not count.
|
|
16
|
+
*/
|
|
17
|
+
import type { Document } from "mongodb";
|
|
18
|
+
import { codeOf } from "./storage-hints.js";
|
|
19
|
+
|
|
20
|
+
/** The stage that splits a change event over the BSON limit into fragments. It is the last stage of a pipeline. */
|
|
21
|
+
export const SPLIT_LARGE_EVENT_STAGE: Readonly<Document> = Object.freeze({ $changeStreamSplitLargeEvent: {} });
|
|
22
|
+
|
|
23
|
+
/** The code of a server that does not know a stage of the pipeline: a server before 6.0.9 does not know the split stage. */
|
|
24
|
+
export const UNKNOWN_STAGE_CODE = 40324;
|
|
25
|
+
|
|
26
|
+
/** The code of a BSON object over the limit: the server cannot give the event. */
|
|
27
|
+
export const OBJECT_TOO_LARGE_CODE = 10334;
|
|
28
|
+
|
|
29
|
+
/** The count of errors with one code at one token after which the fault does not go away. */
|
|
30
|
+
export const SAME_TOKEN_LIMIT = 5;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The codes of the errors that a getMore of a change stream gives in an election or a network fault.
|
|
34
|
+
* The driver resumes the stream after these codes, and the server gives them at each token.
|
|
35
|
+
*/
|
|
36
|
+
const RESUMABLE_CODES: ReadonlySet<number> = new Set([6, 7, 63, 89, 91, 133, 150, 189, 234, 262, 9001, 10107, 11600, 11602, 13388, 13435, 13436]);
|
|
37
|
+
|
|
38
|
+
/** The result of one event of the stream: a whole event, a fragment that waits for the others, or an event without its first fragments. */
|
|
39
|
+
export type JoinResult = { kind: "event"; change: Document } | { kind: "part" } | { kind: "missing"; change: Document; missing: number };
|
|
40
|
+
|
|
41
|
+
/** The joiner of the fragments of a split event. */
|
|
42
|
+
export interface EventJoiner {
|
|
43
|
+
/** This method takes the next event of the stream, and gives the event when it is whole. */
|
|
44
|
+
push(change: Document): JoinResult;
|
|
45
|
+
/** True while fragments of an event wait for the others: the stream is then in the middle of an event. */
|
|
46
|
+
readonly partial: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The fragment fields of an event, or undefined for an event that is not split. */
|
|
50
|
+
function splitOf(change: Document): { fragment: number; of: number } | undefined {
|
|
51
|
+
const split: unknown = change["splitEvent"];
|
|
52
|
+
if (typeof split !== "object" || split === null) return undefined;
|
|
53
|
+
const { fragment, of } = split as Document;
|
|
54
|
+
return typeof fragment === "number" && typeof of === "number" ? { fragment, of } : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** This function joins fragments into one event: the fields of each fragment, the `_id` of the last, and no `splitEvent`. */
|
|
58
|
+
function joined(parts: readonly Document[]): Document {
|
|
59
|
+
const event: Document = Object.assign({}, ...parts);
|
|
60
|
+
event["_id"] = parts[parts.length - 1]!["_id"];
|
|
61
|
+
delete event["splitEvent"];
|
|
62
|
+
return event;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** This function gives a new joiner. Use one joiner for each stream: a new stream starts with no fragments. */
|
|
66
|
+
export function eventJoiner(): EventJoiner {
|
|
67
|
+
let parts: Document[] = [];
|
|
68
|
+
/** The fragments that the stream did not give: the fragments before the first fragment that came. */
|
|
69
|
+
let missing = 0;
|
|
70
|
+
return {
|
|
71
|
+
get partial() {
|
|
72
|
+
return parts.length > 0;
|
|
73
|
+
},
|
|
74
|
+
push(change) {
|
|
75
|
+
const split = splitOf(change);
|
|
76
|
+
if (split === undefined) {
|
|
77
|
+
// An event that is not split ends fragments that wait: the stream cannot give them again
|
|
78
|
+
parts = [];
|
|
79
|
+
missing = 0;
|
|
80
|
+
return { kind: "event", change };
|
|
81
|
+
}
|
|
82
|
+
if (parts.length === 0 || split.fragment !== parts.length + missing + 1) {
|
|
83
|
+
parts = [];
|
|
84
|
+
missing = split.fragment - 1;
|
|
85
|
+
}
|
|
86
|
+
parts.push(change);
|
|
87
|
+
if (split.fragment < split.of) return { kind: "part" };
|
|
88
|
+
const event = joined(parts);
|
|
89
|
+
const lost = missing;
|
|
90
|
+
parts = [];
|
|
91
|
+
missing = 0;
|
|
92
|
+
if (lost > 0) return { kind: "missing", change: { ...event, splitEvent: { missing: lost, of: split.of } }, missing: lost };
|
|
93
|
+
return { kind: "event", change: event };
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** True for an error that the server gives at each token in an election or a network fault. */
|
|
99
|
+
export function isResumable(error: unknown): boolean {
|
|
100
|
+
const labels = (error as { errorLabels?: unknown } | undefined)?.errorLabels;
|
|
101
|
+
if (Array.isArray(labels) && labels.includes("ResumableChangeStreamError")) return true;
|
|
102
|
+
const code = codeOf(error);
|
|
103
|
+
return code === undefined || RESUMABLE_CODES.has(code);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The counter of the errors with one code at one token. */
|
|
107
|
+
export interface SameTokenFaults {
|
|
108
|
+
/** This method counts an error at a token, and gives the count of errors in a row with its code at that token. A resumable error gives 0. */
|
|
109
|
+
count(error: unknown, token: unknown): number;
|
|
110
|
+
/** This method starts the count again: the stream passed the token. */
|
|
111
|
+
reset(): void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** This function gives a new counter of the errors with one code at one token. */
|
|
115
|
+
export function sameTokenFaults(): SameTokenFaults {
|
|
116
|
+
let key: string | undefined;
|
|
117
|
+
let repeats = 0;
|
|
118
|
+
return {
|
|
119
|
+
count(error, token) {
|
|
120
|
+
if (isResumable(error)) {
|
|
121
|
+
key = undefined;
|
|
122
|
+
repeats = 0;
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const next = `${String(codeOf(error))} ${JSON.stringify(token ?? null)}`;
|
|
126
|
+
repeats = next === key ? repeats + 1 : 1;
|
|
127
|
+
key = next;
|
|
128
|
+
return repeats;
|
|
129
|
+
},
|
|
130
|
+
reset() {
|
|
131
|
+
key = undefined;
|
|
132
|
+
repeats = 0;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
type RawStorageWrite,
|
|
93
93
|
type StoredForm,
|
|
94
94
|
} from "./document-storage.js";
|
|
95
|
+
import { SAME_TOKEN_LIMIT, SPLIT_LARGE_EVENT_STAGE, UNKNOWN_STAGE_CODE, eventJoiner, sameTokenFaults } from "./split-events.js";
|
|
95
96
|
import { trackOwnNumbers, type OwnNumbers } from "./own-writes.js";
|
|
96
97
|
|
|
97
98
|
/** The change of one item of a change event of a `MongoDocumentStorage` collection, from `storageChangeOf()`. */
|
|
@@ -197,6 +198,13 @@ export interface WatchStorageChangesOptions {
|
|
|
197
198
|
* @default { initialDelay: ms(250), maxDelay: seconds(30), backoffMultiplier: 2, jitter: 1 }
|
|
198
199
|
*/
|
|
199
200
|
backoff?: ResubscribeBackoff | undefined;
|
|
201
|
+
/**
|
|
202
|
+
* True to end the pipeline of the stream with the stage `$changeStreamSplitLargeEvent`. The watcher
|
|
203
|
+
* then joins the fragments of an event over 16 MiB. With false, such an event fails at each open,
|
|
204
|
+
* and after 5 failures at its token the subscribers get a gap.
|
|
205
|
+
* @default true
|
|
206
|
+
*/
|
|
207
|
+
splitLargeEvents?: boolean | undefined;
|
|
200
208
|
/** The clock of the timers and of the time limit of the own writes (default the clock of the platform). The timers do not keep a process alive. */
|
|
201
209
|
clock?: Clock | undefined;
|
|
202
210
|
/**
|
|
@@ -513,6 +521,9 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
|
|
|
513
521
|
const unplaced = new Set<string>();
|
|
514
522
|
/** The partitions that got a hint: the topics of a gap. */
|
|
515
523
|
const partitions = new Set<string | undefined>();
|
|
524
|
+
/** True while the pipeline ends with the split stage. A server that does not know the stage sets it to false. */
|
|
525
|
+
let split = options.splitLargeEvents !== false;
|
|
526
|
+
const faults = sameTokenFaults();
|
|
516
527
|
let markReady: () => void = () => undefined;
|
|
517
528
|
let failReady: (error: unknown) => void = () => undefined;
|
|
518
529
|
const ready = new Promise<void>((resolve, reject) => {
|
|
@@ -827,6 +838,7 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
|
|
|
827
838
|
const onChange = (change: Document): void => {
|
|
828
839
|
token = change["_id"];
|
|
829
840
|
attempt = 0;
|
|
841
|
+
faults.reset();
|
|
830
842
|
const type = change["operationType"];
|
|
831
843
|
if (type === "drop" || type === "rename" || type === "dropDatabase") return gap(`the collection ${source.collection} had a ${String(type)}`, { operationType: type, resumeToken: true });
|
|
832
844
|
const ns = asDocument(change["ns"]);
|
|
@@ -917,7 +929,7 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
|
|
|
917
929
|
if (token === undefined && opened) gap(`the stream of ${source.collection} opens again without a resume token`, { resumeToken: false });
|
|
918
930
|
let next: ChangeStream;
|
|
919
931
|
try {
|
|
920
|
-
next = db.collection(source.collection).watch([], {
|
|
932
|
+
next = db.collection(source.collection).watch(split ? [SPLIT_LARGE_EVENT_STAGE] : [], {
|
|
921
933
|
fullDocument: "updateLookup",
|
|
922
934
|
fullDocumentBeforeChange: "whenAvailable",
|
|
923
935
|
...(token !== undefined && { startAfter: token }),
|
|
@@ -927,6 +939,9 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
|
|
|
927
939
|
return schedule(`the stream cannot open (${messageOf(error)})`, error);
|
|
928
940
|
}
|
|
929
941
|
stream = next;
|
|
942
|
+
const joiner = eventJoiner();
|
|
943
|
+
/** The token at the open: a reopen at the same token is no success, so the backoff does not start again. */
|
|
944
|
+
const openToken = JSON.stringify(token ?? null);
|
|
930
945
|
let ended = false;
|
|
931
946
|
const end = (reason: string, error?: unknown): void => {
|
|
932
947
|
if (ended) return;
|
|
@@ -942,13 +957,27 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
|
|
|
942
957
|
log("warn", "The server does not know the resume token", { collection: source.collection, code, resumeToken: true }, `the server does not know the resume token of ${source.collection} (code ${code})`);
|
|
943
958
|
token = undefined;
|
|
944
959
|
}
|
|
960
|
+
if (code === UNKNOWN_STAGE_CODE && split) {
|
|
961
|
+
// A server before 6.0.9: the stream opens again without the split stage
|
|
962
|
+
split = false;
|
|
963
|
+
log("warn", "The server does not know the stage $changeStreamSplitLargeEvent", { collection: source.collection, code }, `the server does not know the stage $changeStreamSplitLargeEvent: an event of ${source.collection} over 16 MiB gives a gap`);
|
|
964
|
+
} else if (error !== undefined) {
|
|
965
|
+
const repeats = faults.count(error, token);
|
|
966
|
+
if (repeats >= SAME_TOKEN_LIMIT) {
|
|
967
|
+
// The fault stays at one event: the next stream starts now without the token, and its open gives the gap
|
|
968
|
+
faults.reset();
|
|
969
|
+
token = undefined;
|
|
970
|
+
log("warn", "The stream fails at one token: it opens again without the token", { collection: source.collection, code, repeats }, `the stream of ${source.collection} failed ${repeats} times at one token with the code ${String(code)}: it opens again without the token`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
945
973
|
schedule(`the stream ${reason}`, error);
|
|
946
974
|
};
|
|
947
975
|
next.on("resumeTokenChanged", (resumeToken: unknown) => {
|
|
948
976
|
if (resumeToken === undefined || resumeToken === null) return;
|
|
949
|
-
token
|
|
977
|
+
// In the middle of a split event, the token stays before the event: a stream that opens again gets all its fragments
|
|
978
|
+
if (!joiner.partial) token = resumeToken;
|
|
950
979
|
opened = true;
|
|
951
|
-
attempt = 0;
|
|
980
|
+
if (JSON.stringify(resumeToken) !== openToken) attempt = 0;
|
|
952
981
|
markReady();
|
|
953
982
|
live = true;
|
|
954
983
|
afterQueue();
|
|
@@ -958,15 +987,23 @@ export function watchStorageChanges<T>(storage: MongoDocumentStorage<T>, publish
|
|
|
958
987
|
markReady();
|
|
959
988
|
live = true;
|
|
960
989
|
afterQueue();
|
|
961
|
-
|
|
990
|
+
const result = joiner.push(change);
|
|
991
|
+
if (result.kind === "part") return;
|
|
992
|
+
if (result.kind === "missing") {
|
|
993
|
+
// The stream started after a fragment: the event cannot be read, so the subscribers read the collection in full
|
|
994
|
+
token = result.change["_id"];
|
|
995
|
+
return gap(`a split event of ${source.collection} came without its first ${result.missing} fragments`, { missing: result.missing, resumeToken: true });
|
|
996
|
+
}
|
|
997
|
+
if (result.change["operationType"] === "invalidate") {
|
|
962
998
|
// The collection was dropped or renamed: the stream starts after the invalidate event
|
|
963
|
-
token = change["_id"];
|
|
999
|
+
token = result.change["_id"];
|
|
964
1000
|
return end("ended (invalidate)");
|
|
965
1001
|
}
|
|
966
|
-
onChange(change);
|
|
1002
|
+
onChange(result.change);
|
|
967
1003
|
});
|
|
968
1004
|
next.on("error", (error: Error) => end(`failed: ${error.message}`, error));
|
|
969
|
-
|
|
1005
|
+
// The cursor of the driver tells its close before the error of a failed read. The end waits one turn, so the error comes first and counts
|
|
1006
|
+
next.on("close", () => setImmediate(() => end("closed")));
|
|
970
1007
|
};
|
|
971
1008
|
|
|
972
1009
|
void open();
|