Skip to content

Commit 7e2bf41

Browse files
committed
Revert "update client_view to make progress if many tabs sync at once"
This reverts commit 9f1cb55.
1 parent 9f1cb55 commit 7e2bf41

File tree

4 files changed

+106
-114
lines changed

4 files changed

+106
-114
lines changed

client/src/app.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,17 @@ const App = ({rep, undoManager}: AppProps) => {
5959
'NOT_RECEIVED_FROM_SERVER',
6060
);
6161
const partialSyncComplete = partialSync === 'COMPLETE';
62-
useEffect(() => {
62+
function pull() {
6363
console.log('partialSync', partialSync);
6464
if (!partialSyncComplete) {
65-
rep.pull();
65+
if (document.hidden) {
66+
setTimeout(pull, 1000);
67+
} else {
68+
rep.pull();
69+
}
6670
}
67-
}, [rep, partialSync, partialSyncComplete]);
71+
}
72+
useEffect(pull, [rep, partialSync, partialSyncComplete]);
6873

6974
useEffect(() => {
7075
const ev = new EventSource(`/api/replicache/poke?channel=poke`);

server/src/pull/cvr.ts

+74-95
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,6 @@ export type CVR = {
77
order: number;
88
};
99

10-
const VERSION_TREE_SQL = /*sql*/ `WITH RECURSIVE "version_tree" AS (
11-
SELECT
12-
"client_group_id",
13-
"version",
14-
"parent_version"
15-
FROM
16-
"client_view"
17-
WHERE
18-
"client_group_id" = $1 AND
19-
"version" = $2
20-
21-
UNION ALL
22-
23-
SELECT
24-
cv."client_group_id",
25-
cv."version",
26-
cv."parent_version"
27-
FROM
28-
"client_view" cv
29-
JOIN "version_tree" vt ON vt."parent_version" = cv."version"
30-
WHERE cv."client_group_id" = vt."client_group_id"
31-
)`;
32-
3310
export async function getCVR(
3411
executor: Executor,
3512
clientGroupID: string,
@@ -49,68 +26,61 @@ export async function getCVR(
4926
};
5027
}
5128

52-
export function putCVR(executor: Executor, cvr: CVR, parentVersion: number) {
29+
export function putCVR(executor: Executor, cvr: CVR) {
5330
return executor(
5431
/*sql*/ `INSERT INTO client_view (
5532
"client_group_id",
5633
"client_version",
57-
"version",
58-
"parent_version"
59-
) VALUES ($1, $2, $3, $4) ON CONFLICT ("client_group_id", "version") DO UPDATE SET
34+
"version"
35+
) VALUES ($1, $2, $3) ON CONFLICT ("client_group_id", "version") DO UPDATE SET
6036
client_version = excluded.client_version
6137
`,
62-
[cvr.clientGroupID, cvr.clientVersion, cvr.order, parentVersion],
38+
[cvr.clientGroupID, cvr.clientVersion, cvr.order],
6339
);
6440
}
6541

6642
/**
6743
* Find all rows in a table that are not in our CVR table.
44+
* - either the id is not present
45+
* - or the version is different
6846
*
69-
* Multiple tabs could be trying to sync at the same time with different CVRs.
70-
* We path back through our CVR tree to ensure we send the correct data to each tab.
71-
*
72-
* We can prune branches of the client view tree that are no longer needed.
73-
*
74-
* If we receive a request for a pruned branch we'd track back to find the
75-
* greatest common ancestor.
47+
* We could do cursoring to speed this along.
48+
* I.e., For a given pull sequence, we can keep track of the last id and version.
49+
* A "reset pull" brings cursor back to 0 to deal with privacy or query changes.
7650
*
77-
* We could & should also compact:
78-
* 1. Deletion entries in the client_view_entry table
79-
* 2. Old entity_versions in the client_view_entry table
51+
* But also, if the frontend drives the queries then we'll never actually be fetching
52+
* so much as the frontend queries will be reasonably sized. The frontend queries would also
53+
* be cursored.
8054
*
81-
* Compared to the old approach, the client_view_entry table now grows
82-
* without bound (and requires compaction) since we store a record for each
83-
* entity_version.
55+
* E.g., `SELECT * FROM issue WHERE modified > x OR (modified = y AND id > z) ORDER BY modified, id LIMIT 100`
56+
* Cursored on modified and id.
8457
*
85-
* We wouldn't have to introduce all of this machinery if
86-
* tabs coordinated sync.
58+
* This means our compare against the CVR would not be a full table scan of `issue`.
8759
*/
8860
export function findUnsentItems(
8961
executor: Executor,
9062
table: keyof typeof TableOrdinal,
9163
clientGroupID: string,
92-
version: number,
64+
order: number,
9365
limit: number,
9466
) {
9567
// The query used below is the fastest.
9668
// Other query forms that were tried:
9769
// 1. 10x slower: SELECT * FROM table WHERE NOT EXISTS (SELECT 1 FROM client_view_entry ...)
9870
// 2. 2x slower: SELECT id, version FROM table EXCEPT SELECT entity_id, entity_version FROM client_view_entry ...
9971
// 3. SELECT * FROM table LEFT JOIN client_view_entry ...
100-
const sql = /*sql*/ `
101-
${VERSION_TREE_SQL}
102-
SELECT *
103-
FROM "${table}" t
104-
WHERE (t."id", t."version") NOT IN (
105-
SELECT "entity_id", "entity_version"
106-
FROM "client_view_entry"
107-
WHERE "client_view_version" IN (SELECT "version" FROM "version_tree") AND
108-
"client_group_id" = $1 AND
109-
"entity" = $3
110-
)
111-
LIMIT $4;`;
72+
const sql = /*sql*/ `SELECT *
73+
FROM "${table}" t
74+
WHERE (t."id", t."version") NOT IN (
75+
SELECT "entity_id", "entity_version"
76+
FROM "client_view_entry"
77+
WHERE "client_group_id" = $1
78+
AND "client_view_version" <= $2
79+
AND "entity" = $3
80+
)
81+
LIMIT $4;`;
11282

113-
const params = [clientGroupID, version, TableOrdinal[table], limit];
83+
const params = [clientGroupID, order, TableOrdinal[table], limit];
11484
return executor(sql, params);
11585
}
11686

@@ -124,40 +94,45 @@ export function findDeletions(
12494
executor: Executor,
12595
table: keyof typeof TableOrdinal,
12696
clientGroupID: string,
127-
version: number,
97+
order: number,
12898
limit: number,
12999
) {
130-
const sql = /*sql*/ `${VERSION_TREE_SQL}
131-
SELECT DISTINCT("entity_id") FROM "client_view_entry" as cve WHERE
132-
cve."client_group_id" = $1 AND
133-
cve."entity" = $3 AND
134-
cve."client_view_version" IN (SELECT "version" FROM "version_tree") AND
135-
-- check that the entity is missing from the base table
136-
NOT EXISTS (
137-
SELECT 1 FROM "${table}" WHERE id = cve."entity_id"
100+
// Find rows that are in the CVR table but not in the actual table.
101+
// Exclude rows that have a delete entry already.
102+
// TODO: Maybe we can remove the second `NOT EXISTS` if we prune the CVR table.
103+
// This pruning would mean that we need to record the delete against the
104+
// current CVR rather than next CVR. If a request comes in for that prior CVR,
105+
// we return the stored delete records and do not compute deletes.
106+
return executor(
107+
/*sql*/ `SELECT "entity_id" FROM "client_view_entry"
108+
WHERE "client_view_entry"."entity" = $1 AND NOT EXISTS (
109+
SELECT 1 FROM "${table}" WHERE id = "client_view_entry"."entity_id"
138110
) AND
139-
-- check that we didn't already record this deletion
140-
-- and that the row wasn't later resurrected and sent if we did record a deletion
141-
NOT EXISTS (
142-
SELECT 1
143-
FROM client_view_entry as cve2 LEFT JOIN client_view_entry as cve3
144-
ON (
145-
cve2.entity_id = cve3.entity_id AND
146-
cve2.client_view_version < cve3.client_view_version AND
147-
cve2.client_group_id = cve3.client_group_id AND
148-
cve2.entity = cve3.entity
149-
)
150-
WHERE
151-
cve3.entity_id IS NULL AND
152-
cve2.entity_id = cve.entity_id AND
153-
cve2.entity_version IS NULL AND
154-
cve2.client_group_id = $1 AND
155-
cve2.entity = $3 AND
156-
cve2.client_view_version IN (SELECT "version" FROM "version_tree")
157-
)
158-
LIMIT $4`;
159-
const vars = [clientGroupID, version, TableOrdinal[table], limit];
160-
return executor(sql, vars);
111+
"client_view_entry"."client_group_id" = $2 AND
112+
"client_view_entry"."client_view_version" <= $3
113+
AND NOT EXISTS (
114+
SELECT 1 FROM "client_view_delete_entry" WHERE "client_view_delete_entry"."entity" = $1 AND "client_view_delete_entry"."entity_id" = "client_view_entry"."entity_id"
115+
AND "client_view_delete_entry"."client_group_id" = $2 AND "client_view_delete_entry"."client_view_version" <= $3
116+
) LIMIT $4`,
117+
[TableOrdinal[table], clientGroupID, order, limit],
118+
);
119+
}
120+
121+
export async function dropCVREntries(
122+
executor: Executor,
123+
clientGroupID: string,
124+
order: number,
125+
) {
126+
await Promise.all([
127+
executor(
128+
/*sql*/ `DELETE FROM "client_view_entry" WHERE "client_group_id" = $1 AND "client_view_version" > $2`,
129+
[clientGroupID, order],
130+
),
131+
executor(
132+
/*sql*/ `DELETE FROM "client_view_delete_entry" WHERE "client_group_id" = $1 AND "client_view_version" > $2`,
133+
[clientGroupID, order],
134+
),
135+
]);
161136
}
162137

163138
export async function recordUpdates(
@@ -196,7 +171,12 @@ export async function recordUpdates(
196171
"entity",
197172
"entity_id",
198173
"entity_version"
199-
) VALUES ${placeholders.join(', ')}`,
174+
) VALUES ${placeholders.join(
175+
', ',
176+
)} ON CONFLICT ("client_group_id", "entity", "entity_id") DO UPDATE SET
177+
"entity_version" = excluded."entity_version",
178+
"client_view_version" = excluded."client_view_version"
179+
`,
200180
values,
201181
);
202182
}
@@ -225,22 +205,21 @@ export async function recordDeletes(
225205
}
226206
const placeholders = [];
227207
const values = [];
228-
const stride = 5;
208+
const stride = 4;
229209
for (let i = 0; i < ids.length; i++) {
230210
placeholders.push(
231211
`($${i * stride + 1}, $${i * stride + 2}, $${i * stride + 3}, $${
232212
i * stride + 4
233-
}, $${i * stride + 5})`,
213+
})`,
234214
);
235-
values.push(clientGroupID, order, TableOrdinal[table], ids[i], null);
215+
values.push(clientGroupID, order, TableOrdinal[table], ids[i]);
236216
}
237217
await executor(
238-
/*sql*/ `INSERT INTO client_view_entry (
218+
/*sql*/ `INSERT INTO client_view_delete_entry (
239219
"client_group_id",
240220
"client_view_version",
241221
"entity",
242-
"entity_id",
243-
"entity_version"
222+
"entity_id"
244223
) VALUES ${placeholders.join(', ')}`,
245224
values,
246225
);

server/src/pull/pull.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import {transact} from '../pg';
44
import {getClientGroupForUpdate, putClientGroup, searchClients} from '../data';
55
import type Express from 'express';
66
import {hasNextPage, isPageEmpty, readNextPage} from './next-page';
7-
import {CVR, getCVR, putCVR, recordDeletes, recordUpdates} from './cvr';
7+
import {
8+
CVR,
9+
dropCVREntries,
10+
getCVR,
11+
putCVR,
12+
recordDeletes,
13+
recordUpdates,
14+
} from './cvr';
815
import {PartialSyncState, PARTIAL_SYNC_STATE_KEY} from 'shared';
916

1017
const cookieSchema = z.object({
@@ -44,6 +51,16 @@ export async function pull(
4451
order: 0,
4552
};
4653

54+
// Drop any cvr entries greater than the order we received.
55+
// Getting an old order from the client means that the client is possibly missing the data
56+
// from future orders.
57+
//
58+
// Why do we delete later CVRs?
59+
// Since we are sharing CVR data across orders (CVR_n = CVR_n-1 + current_pull).
60+
// To keep greater CVRs around, which may have never been received, means that the next CVR would
61+
// indicate that the data was received when it was not.
62+
await dropCVREntries(executor, clientGroupID, baseCVR.order);
63+
4764
const [clientChanges, nextPage] = await Promise.all([
4865
searchClients(executor, {
4966
clientGroupID,
@@ -115,7 +132,7 @@ export async function pull(
115132

116133
await Promise.all([
117134
putClientGroup(executor, nextClientGroupRecord),
118-
putCVR(executor, nextCVR, baseCVR.order),
135+
putCVR(executor, nextCVR),
119136
]);
120137

121138
await Promise.all([

server/src/schema.ts

+5-14
Original file line numberDiff line numberDiff line change
@@ -73,31 +73,22 @@ export async function createSchemaVersion1(executor: Executor) {
7373
await executor(/*sql*/ `CREATE TABLE "client_view" (
7474
"client_group_id" VARCHAR(36) NOT NULL,
7575
"version" INTEGER NOT NULL,
76-
"parent_version" INTEGER NOT NULL,
7776
"client_version" INTEGER NOT NULL,
7877
PRIMARY KEY ("client_group_id", "version")
7978
)`);
8079

81-
await executor(
82-
/*sql*/ `CREATE INDEX client_view_parent ON client_view (client_group_id, parent_version)`,
83-
);
84-
8580
await executor(/*sql*/ `CREATE TABLE "client_view_entry" (
8681
"client_group_id" VARCHAR(36) NOT NULL,
8782
"client_view_version" INTEGER NOT NULL,
8883
"entity" INTEGER NOT NULL,
8984
"entity_id" VARCHAR(36) NOT NULL,
90-
"entity_version" INTEGER
85+
"entity_version" INTEGER NOT NULL,
86+
-- unique by client_group_id, entity, entity_id
87+
-- 1. A missing row version is semantically the same as a behind row version
88+
-- 2. Our CVR is recursive. CVR_n = CVR_n-1 + (changes since CVR_n-1)
89+
PRIMARY KEY ("client_group_id", "entity", "entity_id")
9190
)`);
9291

93-
// This index is for quick lookup of everything in a client view.
94-
// - get entries for the specific client
95-
// - for the specific table
96-
// - at the specific client_view version
97-
await executor(
98-
/*sql*/ `CREATE INDEX client_view_entry_lookup ON client_view_entry (client_group_id, entity, client_view_version)`,
99-
);
100-
10192
await executor(/*sql*/ `CREATE TABLE "client_view_delete_entry" (
10293
"client_group_id" VARCHAR(36) NOT NULL,
10394
"client_view_version" INTEGER NOT NULL,

0 commit comments

Comments
 (0)