Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 9614d3c

Browse files
authored
Merge pull request #6089 from matrix-org/erikj/cleanup_user_ips
Move last seen info into devices table
2 parents a4f3ca4 + d2bd0bc commit 9614d3c

File tree

4 files changed

+179
-50
lines changed

4 files changed

+179
-50
lines changed

changelog.d/6089.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Move last seen info into devices table.

synapse/storage/client_ips.py

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ def __init__(self, db_conn, hs):
8585
"user_ips_drop_nonunique_index", self._remove_user_ip_nonunique
8686
)
8787

88+
# Update the last seen info in devices.
89+
self.register_background_update_handler(
90+
"devices_last_seen", self._devices_last_seen_update
91+
)
92+
8893
# (user_id, access_token, ip,) -> (user_agent, device_id, last_seen)
8994
self._batch_row_update = {}
9095

@@ -354,6 +359,21 @@ def _update_client_ips_batch_txn(self, txn, to_update):
354359
},
355360
lock=False,
356361
)
362+
363+
# Technically an access token might not be associated with
364+
# a device so we need to check.
365+
if device_id:
366+
self._simple_upsert_txn(
367+
txn,
368+
table="devices",
369+
keyvalues={"user_id": user_id, "device_id": device_id},
370+
values={
371+
"user_agent": user_agent,
372+
"last_seen": last_seen,
373+
"ip": ip,
374+
},
375+
lock=False,
376+
)
357377
except Exception as e:
358378
# Failed to upsert, log and continue
359379
logger.error("Failed to insert client IP %r: %r", entry, e)
@@ -372,19 +392,14 @@ def get_last_client_ip_by_device(self, user_id, device_id):
372392
keys giving the column names
373393
"""
374394

375-
res = yield self.runInteraction(
376-
"get_last_client_ip_by_device",
377-
self._get_last_client_ip_by_device_txn,
378-
user_id,
379-
device_id,
380-
retcols=(
381-
"user_id",
382-
"access_token",
383-
"ip",
384-
"user_agent",
385-
"device_id",
386-
"last_seen",
387-
),
395+
keyvalues = {"user_id": user_id}
396+
if device_id is not None:
397+
keyvalues["device_id"] = device_id
398+
399+
res = yield self._simple_select_list(
400+
table="devices",
401+
keyvalues=keyvalues,
402+
retcols=("user_id", "ip", "user_agent", "device_id", "last_seen"),
388403
)
389404

390405
ret = {(d["user_id"], d["device_id"]): d for d in res}
@@ -403,42 +418,6 @@ def get_last_client_ip_by_device(self, user_id, device_id):
403418
}
404419
return ret
405420

406-
@classmethod
407-
def _get_last_client_ip_by_device_txn(cls, txn, user_id, device_id, retcols):
408-
where_clauses = []
409-
bindings = []
410-
if device_id is None:
411-
where_clauses.append("user_id = ?")
412-
bindings.extend((user_id,))
413-
else:
414-
where_clauses.append("(user_id = ? AND device_id = ?)")
415-
bindings.extend((user_id, device_id))
416-
417-
if not where_clauses:
418-
return []
419-
420-
inner_select = (
421-
"SELECT MAX(last_seen) mls, user_id, device_id FROM user_ips "
422-
"WHERE %(where)s "
423-
"GROUP BY user_id, device_id"
424-
) % {"where": " OR ".join(where_clauses)}
425-
426-
sql = (
427-
"SELECT %(retcols)s FROM user_ips "
428-
"JOIN (%(inner_select)s) ips ON"
429-
" user_ips.last_seen = ips.mls AND"
430-
" user_ips.user_id = ips.user_id AND"
431-
" (user_ips.device_id = ips.device_id OR"
432-
" (user_ips.device_id IS NULL AND ips.device_id IS NULL)"
433-
" )"
434-
) % {
435-
"retcols": ",".join("user_ips." + c for c in retcols),
436-
"inner_select": inner_select,
437-
}
438-
439-
txn.execute(sql, bindings)
440-
return cls.cursor_to_dict(txn)
441-
442421
@defer.inlineCallbacks
443422
def get_user_ip_and_agents(self, user):
444423
user_id = user.to_string()
@@ -470,3 +449,50 @@ def get_user_ip_and_agents(self, user):
470449
}
471450
for (access_token, ip), (user_agent, last_seen) in iteritems(results)
472451
)
452+
453+
@defer.inlineCallbacks
454+
def _devices_last_seen_update(self, progress, batch_size):
455+
"""Background update to insert last seen info into devices table
456+
"""
457+
458+
last_user_id = progress.get("last_user_id", "")
459+
last_device_id = progress.get("last_device_id", "")
460+
461+
def _devices_last_seen_update_txn(txn):
462+
sql = """
463+
SELECT u.last_seen, u.ip, u.user_agent, user_id, device_id FROM devices
464+
INNER JOIN user_ips AS u USING (user_id, device_id)
465+
WHERE user_id > ? OR (user_id = ? AND device_id > ?)
466+
ORDER BY user_id ASC, device_id ASC
467+
LIMIT ?
468+
"""
469+
txn.execute(sql, (last_user_id, last_user_id, last_device_id, batch_size))
470+
471+
rows = txn.fetchall()
472+
if not rows:
473+
return 0
474+
475+
sql = """
476+
UPDATE devices
477+
SET last_seen = ?, ip = ?, user_agent = ?
478+
WHERE user_id = ? AND device_id = ?
479+
"""
480+
txn.execute_batch(sql, rows)
481+
482+
_, _, _, user_id, device_id = rows[-1]
483+
self._background_update_progress_txn(
484+
txn,
485+
"devices_last_seen",
486+
{"last_user_id": user_id, "last_device_id": device_id},
487+
)
488+
489+
return len(rows)
490+
491+
updated = yield self.runInteraction(
492+
"_devices_last_seen_update", _devices_last_seen_update_txn
493+
)
494+
495+
if not updated:
496+
yield self._end_background_update("devices_last_seen")
497+
498+
return updated
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* Copyright 2019 Matrix.org Foundation CIC
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- Track last seen information for a device in the devices table, rather
17+
-- than relying on it being in the user_ips table (which we want to be able
18+
-- to purge old entries from)
19+
ALTER TABLE devices ADD COLUMN last_seen BIGINT;
20+
ALTER TABLE devices ADD COLUMN ip TEXT;
21+
ALTER TABLE devices ADD COLUMN user_agent TEXT;
22+
23+
INSERT INTO background_updates (update_name, progress_json) VALUES
24+
('devices_last_seen', '{}');

tests/storage/test_client_ips.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ def test_insert_new_client_ip(self):
5555
{
5656
"user_id": user_id,
5757
"device_id": "device_id",
58-
"access_token": "access_token",
5958
"ip": "ip",
6059
"user_agent": "user_agent",
6160
"last_seen": 12345678000,
@@ -201,6 +200,85 @@ def test_updating_monthly_active_user_when_space(self):
201200
active = self.get_success(self.store.user_last_seen_monthly_active(user_id))
202201
self.assertTrue(active)
203202

203+
def test_devices_last_seen_bg_update(self):
204+
# First make sure we have completed all updates.
205+
while not self.get_success(self.store.has_completed_background_updates()):
206+
self.get_success(self.store.do_next_background_update(100), by=0.1)
207+
208+
# Insert a user IP
209+
user_id = "@user:id"
210+
self.get_success(
211+
self.store.insert_client_ip(
212+
user_id, "access_token", "ip", "user_agent", "device_id"
213+
)
214+
)
215+
216+
# Force persisting to disk
217+
self.reactor.advance(200)
218+
219+
# But clear the associated entry in devices table
220+
self.get_success(
221+
self.store._simple_update(
222+
table="devices",
223+
keyvalues={"user_id": user_id, "device_id": "device_id"},
224+
updatevalues={"last_seen": None, "ip": None, "user_agent": None},
225+
desc="test_devices_last_seen_bg_update",
226+
)
227+
)
228+
229+
# We should now get nulls when querying
230+
result = self.get_success(
231+
self.store.get_last_client_ip_by_device(user_id, "device_id")
232+
)
233+
234+
r = result[(user_id, "device_id")]
235+
self.assertDictContainsSubset(
236+
{
237+
"user_id": user_id,
238+
"device_id": "device_id",
239+
"ip": None,
240+
"user_agent": None,
241+
"last_seen": None,
242+
},
243+
r,
244+
)
245+
246+
# Register the background update to run again.
247+
self.get_success(
248+
self.store._simple_insert(
249+
table="background_updates",
250+
values={
251+
"update_name": "devices_last_seen",
252+
"progress_json": "{}",
253+
"depends_on": None,
254+
},
255+
)
256+
)
257+
258+
# ... and tell the DataStore that it hasn't finished all updates yet
259+
self.store._all_done = False
260+
261+
# Now let's actually drive the updates to completion
262+
while not self.get_success(self.store.has_completed_background_updates()):
263+
self.get_success(self.store.do_next_background_update(100), by=0.1)
264+
265+
# We should now get the correct result again
266+
result = self.get_success(
267+
self.store.get_last_client_ip_by_device(user_id, "device_id")
268+
)
269+
270+
r = result[(user_id, "device_id")]
271+
self.assertDictContainsSubset(
272+
{
273+
"user_id": user_id,
274+
"device_id": "device_id",
275+
"ip": "ip",
276+
"user_agent": "user_agent",
277+
"last_seen": 0,
278+
},
279+
r,
280+
)
281+
204282

205283
class ClientIpAuthTestCase(unittest.HomeserverTestCase):
206284

0 commit comments

Comments
 (0)