Skip to content

Commit e243af5

Browse files
showelltimabbott
authored andcommitted
refactor: Extract get_user_set in peer_data.
We now use the same code in all places to get the bucket of user_ids that correspond to a stream, and we consistently treat a stream as having zero subscribers, not an undefined number of subscribers, in the hypothetical case of us asking about a stream that we're not tracking. The behavior for untracked streams has always been problematic, since if a stream is untracked, all bets are off. So now if we don't "track" the stream, the subscriber count is zero. None of our callers distinguish between undefined and zero. And we just consider the stream to be subscribed by a user when add_subscriber is called, even if we haven't been told by stream_data to track the stream. (We also stop returning true/false from add_subscriber, since only test code was looking at it.) We protect against the most likely source of internal-to-the-frontend bugs by adding the assert_number() call. We generally have to assume that the server is sending us sensible data at page load time, or all bets are off. And we have good protections in place for unknown ids in our dispatch code for peer_add/peer_remove events.
1 parent 5228146 commit e243af5

File tree

4 files changed

+74
-96
lines changed

4 files changed

+74
-96
lines changed

frontend_tests/node_tests/compose.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,9 @@ run_test("warn_if_private_stream_is_linked", () => {
10181018
denmark = {
10191019
invite_only: true,
10201020
name: "Denmark",
1021+
stream_id: 22,
10211022
};
1023+
stream_data.add_sub(denmark);
10221024

10231025
compose.warn_if_private_stream_is_linked(denmark);
10241026
assert.equal($("#compose_private_stream_alert").visible(), true);

frontend_tests/node_tests/peer_data.js

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
const {strict: assert} = require("assert");
1010

11-
const {set_global, with_field, zrequire} = require("../zjsunit/namespace");
11+
const {set_global, zrequire} = require("../zjsunit/namespace");
1212
const {run_test} = require("../zjsunit/test");
1313

1414
const peer_data = zrequire("peer_data");
@@ -118,8 +118,7 @@ run_test("subscribers", () => {
118118
assert(!stream_data.is_user_subscribed(sub.stream_id, brutus.user_id));
119119

120120
// add
121-
let ok = peer_data.add_subscriber(sub.stream_id, brutus.user_id);
122-
assert(ok);
121+
peer_data.add_subscriber(sub.stream_id, brutus.user_id);
123122
assert(stream_data.is_user_subscribed(sub.stream_id, brutus.user_id));
124123
sub = stream_data.get_sub("Rome");
125124
assert.equal(peer_data.get_subscriber_count(sub.stream_id), 1);
@@ -134,7 +133,7 @@ run_test("subscribers", () => {
134133
assert.equal(peer_data.get_subscriber_count(sub.stream_id), 1);
135134

136135
// remove
137-
ok = peer_data.remove_subscriber(sub.stream_id, brutus.user_id);
136+
let ok = peer_data.remove_subscriber(sub.stream_id, brutus.user_id);
138137
assert(ok);
139138
assert(!stream_data.is_user_subscribed(sub.stream_id, brutus.user_id));
140139
sub = stream_data.get_sub("Rome");
@@ -144,15 +143,15 @@ run_test("subscribers", () => {
144143

145144
blueslip.expect("warn", "Undefined user_id passed to function is_user_subscribed");
146145
assert.equal(stream_data.is_user_subscribed(sub.stream_id, undefined), undefined);
146+
blueslip.reset();
147147

148148
// Verify noop for bad stream when removing subscriber
149149
const bad_stream_id = 999999;
150-
blueslip.expect(
151-
"warn",
152-
"We got a remove_subscriber call for an untracked stream " + bad_stream_id,
153-
);
150+
blueslip.expect("warn", "We called get_user_set for an untracked stream: " + bad_stream_id);
151+
blueslip.expect("warn", "We tried to remove invalid subscriber: 104");
154152
ok = peer_data.remove_subscriber(bad_stream_id, brutus.user_id);
155153
assert(!ok);
154+
blueslip.reset();
156155

157156
// verify that removing an already-removed subscriber is a noop
158157
blueslip.expect("warn", "We tried to remove invalid subscriber: 104");
@@ -161,6 +160,7 @@ run_test("subscribers", () => {
161160
assert(!stream_data.is_user_subscribed(sub.stream_id, brutus.user_id));
162161
sub = stream_data.get_sub("Rome");
163162
assert.equal(peer_data.get_subscriber_count(sub.stream_id), 0);
163+
blueslip.reset();
164164

165165
// Verify defensive code in set_subscribers, where the second parameter
166166
// can be undefined.
@@ -172,8 +172,7 @@ run_test("subscribers", () => {
172172
// Verify that we noop and don't crash when unsubscribed.
173173
sub.subscribed = false;
174174
stream_data.update_calculated_fields(sub);
175-
ok = peer_data.add_subscriber(sub.stream_id, brutus.user_id);
176-
assert(ok);
175+
peer_data.add_subscriber(sub.stream_id, brutus.user_id);
177176
assert.equal(stream_data.is_user_subscribed(sub.stream_id, brutus.user_id), true);
178177
peer_data.remove_subscriber(sub.stream_id, brutus.user_id);
179178
assert.equal(stream_data.is_user_subscribed(sub.stream_id, brutus.user_id), false);
@@ -190,17 +189,18 @@ run_test("subscribers", () => {
190189
assert.equal(stream_data.is_user_subscribed(sub.stream_id, brutus.user_id), undefined);
191190
peer_data.remove_subscriber(sub.stream_id, brutus.user_id);
192191
assert.equal(stream_data.is_user_subscribed(sub.stream_id, brutus.user_id), undefined);
193-
194-
// Verify that we don't crash and return false for a bad stream.
195-
blueslip.expect("warn", "We got an add_subscriber call for an untracked stream: 9999999");
196-
ok = peer_data.add_subscriber(9999999, brutus.user_id);
197-
assert(!ok);
198-
199-
// Verify that we don't crash and return false for a bad user id.
200-
blueslip.expect("error", "Unknown user_id in get_by_user_id: 9999999");
201-
blueslip.expect("error", "We tried to add invalid subscriber: 9999999");
202-
ok = peer_data.add_subscriber(sub.stream_id, 9999999);
203-
assert(!ok);
192+
blueslip.reset();
193+
194+
// Verify that we don't crash for a bad stream.
195+
blueslip.expect("warn", "We called get_user_set for an untracked stream: 9999999");
196+
peer_data.add_subscriber(9999999, brutus.user_id);
197+
blueslip.reset();
198+
199+
// Verify that we don't crash for a bad user id.
200+
blueslip.expect("error", "Unknown user_id in get_by_user_id: 88888");
201+
blueslip.expect("warn", "We tried to add invalid subscriber: 88888");
202+
peer_data.add_subscriber(sub.stream_id, 88888);
203+
blueslip.reset();
204204
});
205205

206206
run_test("get_subscriber_count", () => {
@@ -211,8 +211,8 @@ run_test("get_subscriber_count", () => {
211211
};
212212
stream_data.clear_subscriptions();
213213

214-
blueslip.expect("warn", "We got a get_subscriber_count call for an untracked stream: 102");
215-
assert.equal(peer_data.get_subscriber_count(india.stream_id), undefined);
214+
blueslip.expect("warn", "We called get_user_set for an untracked stream: 102");
215+
assert.equal(peer_data.get_subscriber_count(india.stream_id), 0);
216216

217217
stream_data.add_sub(india);
218218
assert.equal(peer_data.get_subscriber_count(india.stream_id), 0);
@@ -249,13 +249,6 @@ run_test("is_subscriber_subset", () => {
249249
const sub_b = make_sub(302, [2, 3]);
250250
const sub_c = make_sub(303, [1, 2, 3]);
251251

252-
// The bogus case should not come up in normal
253-
// use.
254-
// We simply punt on any calculation if
255-
// a stream has no subscriber info (like
256-
// maybe Zephyr?).
257-
const bogus = {}; // no subscribers
258-
259252
const matrix = [
260253
[sub_a, sub_a, true],
261254
[sub_a, sub_b, false],
@@ -266,33 +259,21 @@ run_test("is_subscriber_subset", () => {
266259
[sub_c, sub_a, false],
267260
[sub_c, sub_b, false],
268261
[sub_c, sub_c, true],
269-
[bogus, bogus, false],
270262
];
271263

272264
for (const row of matrix) {
273265
assert.equal(peer_data.is_subscriber_subset(row[0].stream_id, row[1].stream_id), row[2]);
274266
}
275-
});
276267

277-
run_test("warn if subscribers are missing", () => {
278-
// This should only happen in this contrived test situation.
279-
stream_data.clear_subscriptions();
280-
const sub = {
281-
name: "test",
282-
stream_id: 3,
283-
can_access_subscribers: true,
284-
};
285-
286-
with_field(
287-
stream_data,
288-
"get_sub_by_id",
289-
() => sub,
290-
() => {
291-
blueslip.expect("warn", "We called is_user_subscribed for an untracked stream: 3");
292-
stream_data.is_user_subscribed(sub.stream_id, me.user_id);
293-
294-
blueslip.expect("warn", "We called get_subscribers for an untracked stream: 3");
295-
assert.deepEqual(peer_data.get_subscribers(sub.stream_id), []);
296-
},
297-
);
268+
// Two untracked streams should never be passed into us.
269+
blueslip.expect("warn", "We called get_user_set for an untracked stream: 88888");
270+
blueslip.expect("warn", "We called get_user_set for an untracked stream: 99999");
271+
peer_data.is_subscriber_subset(99999, 88888);
272+
blueslip.reset();
273+
274+
// Warn about hypothetical undefined stream_ids.
275+
blueslip.expect("error", "You must pass ids as numbers to peer_data. id = undefined");
276+
blueslip.expect("warn", "We called get_user_set for an untracked stream: undefined");
277+
peer_data.is_subscriber_subset(undefined, sub_a.stream_id);
278+
blueslip.reset();
298279
});

frontend_tests/node_tests/stream_events.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ run_test("remove_deactivated_user_from_all_streams", () => {
420420
assert(!stream_data.is_user_subscribed(dev_help.stream_id, george.user_id));
421421

422422
// verify that deactivating user should unsubscribe user from all streams
423-
assert(peer_data.add_subscriber(dev_help.stream_id, george.user_id));
423+
peer_data.add_subscriber(dev_help.stream_id, george.user_id);
424424
assert(stream_data.is_user_subscribed(dev_help.stream_id, george.user_id));
425425

426426
stream_events.remove_deactivated_user_from_all_streams(george.user_id);

static/js/peer_data.js

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ const people = require("./people");
77
// clear_subscriptions.
88
let stream_subscribers;
99

10+
function assert_number(id) {
11+
if (typeof id !== "number") {
12+
blueslip.error(`You must pass ids as numbers to peer_data. id = ${id}`);
13+
}
14+
}
15+
16+
function get_user_set(stream_id) {
17+
assert_number(stream_id);
18+
19+
// This is an internal function to get the LazySet of users.
20+
// We create one on the fly as necessary, but we warn in that case.
21+
let subscribers = stream_subscribers.get(stream_id);
22+
23+
if (subscribers === undefined) {
24+
blueslip.warn("We called get_user_set for an untracked stream: " + stream_id);
25+
subscribers = new LazySet([]);
26+
stream_subscribers.set(stream_id, subscribers);
27+
}
28+
29+
return subscribers;
30+
}
31+
1032
export function clear() {
1133
stream_subscribers = new Map();
1234
}
@@ -18,14 +40,10 @@ export function maybe_clear_subscribers(stream_id) {
1840
}
1941

2042
export function is_subscriber_subset(stream_id1, stream_id2) {
21-
const sub1_set = stream_subscribers.get(stream_id1);
22-
const sub2_set = stream_subscribers.get(stream_id2);
23-
24-
if (sub1_set && sub2_set) {
25-
return Array.from(sub1_set.keys()).every((key) => sub2_set.has(key));
26-
}
43+
const sub1_set = get_user_set(stream_id1);
44+
const sub2_set = get_user_set(stream_id2);
2745

28-
return false;
46+
return Array.from(sub1_set.keys()).every((key) => sub2_set.has(key));
2947
}
3048

3149
export function potential_subscribers(stream_id) {
@@ -63,23 +81,14 @@ export function potential_subscribers(stream_id) {
6381
}
6482

6583
export function get_subscriber_count(stream_id) {
66-
const subscribers = stream_subscribers.get(stream_id);
67-
68-
if (!subscribers) {
69-
blueslip.warn("We got a get_subscriber_count call for an untracked stream: " + stream_id);
70-
return undefined;
71-
}
72-
84+
const subscribers = get_user_set(stream_id);
7385
return subscribers.size;
7486
}
7587

7688
export function get_subscribers(stream_id) {
77-
const subscribers = stream_subscribers.get(stream_id);
78-
79-
if (typeof subscribers === "undefined") {
80-
blueslip.warn("We called get_subscribers for an untracked stream: " + stream_id);
81-
return [];
82-
}
89+
// This is our external interface for callers who just
90+
// want an array of user_ids who are subscribed to a stream.
91+
const subscribers = get_user_set(stream_id);
8392

8493
return Array.from(subscribers.keys());
8594
}
@@ -91,27 +100,18 @@ export function set_subscribers(stream_id, user_ids) {
91100
}
92101

93102
export function add_subscriber(stream_id, user_id) {
94-
const subscribers = stream_subscribers.get(stream_id);
95-
if (typeof subscribers === "undefined") {
96-
blueslip.warn("We got an add_subscriber call for an untracked stream: " + stream_id);
97-
return false;
98-
}
103+
// If stream_id/user_id are unknown to us, we will
104+
// still track it, but we will warn.
105+
const subscribers = get_user_set(stream_id);
99106
const person = people.get_by_user_id(user_id);
100107
if (person === undefined) {
101-
blueslip.error("We tried to add invalid subscriber: " + user_id);
102-
return false;
108+
blueslip.warn("We tried to add invalid subscriber: " + user_id);
103109
}
104110
subscribers.add(user_id);
105-
106-
return true;
107111
}
108112

109113
export function remove_subscriber(stream_id, user_id) {
110-
const subscribers = stream_subscribers.get(stream_id);
111-
if (typeof subscribers === "undefined") {
112-
blueslip.warn("We got a remove_subscriber call for an untracked stream " + stream_id);
113-
return false;
114-
}
114+
const subscribers = get_user_set(stream_id);
115115
if (!subscribers.has(user_id)) {
116116
blueslip.warn("We tried to remove invalid subscriber: " + user_id);
117117
return false;
@@ -125,7 +125,7 @@ export function remove_subscriber(stream_id, user_id) {
125125
export function bulk_add_subscribers({stream_ids, user_ids}) {
126126
// We rely on our callers to validate stream_ids and user_ids.
127127
for (const stream_id of stream_ids) {
128-
const subscribers = stream_subscribers.get(stream_id) || set_subscribers(stream_id);
128+
const subscribers = get_user_set(stream_id);
129129
for (const user_id of user_ids) {
130130
subscribers.add(user_id);
131131
}
@@ -135,7 +135,7 @@ export function bulk_add_subscribers({stream_ids, user_ids}) {
135135
export function bulk_remove_subscribers({stream_ids, user_ids}) {
136136
// We rely on our callers to validate stream_ids and user_ids.
137137
for (const stream_id of stream_ids) {
138-
const subscribers = stream_subscribers.get(stream_id) || set_subscribers(stream_id);
138+
const subscribers = get_user_set(stream_id);
139139
for (const user_id of user_ids) {
140140
subscribers.delete(user_id);
141141
}
@@ -146,11 +146,6 @@ export function is_user_subscribed(stream_id, user_id) {
146146
// Most callers should call stream_data.is_user_subscribed,
147147
// which does additional checks.
148148

149-
const subscribers = stream_subscribers.get(stream_id);
150-
if (typeof subscribers === "undefined") {
151-
blueslip.warn("We called is_user_subscribed for an untracked stream: " + stream_id);
152-
return false;
153-
}
154-
149+
const subscribers = get_user_set(stream_id);
155150
return subscribers.has(user_id);
156151
}

0 commit comments

Comments
 (0)