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

Add option to track MAU stats (but not limit people) #3830

Merged
merged 11 commits into from
Nov 15, 2018
1 change: 1 addition & 0 deletions changelog.d/3830.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add option to track MAU stats (but not limit people)
2 changes: 1 addition & 1 deletion synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ def generate_monthly_active_users():
current_mau_count = 0
reserved_count = 0
store = hs.get_datastore()
if hs.config.limit_usage_by_mau:
if hs.config.limit_usage_by_mau or hs.config.mau_stats_only:
current_mau_count = yield store.get_monthly_active_count()
reserved_count = yield store.get_registered_reserved_users_count()
current_mau_gauge.set(float(current_mau_count))
Expand Down
6 changes: 6 additions & 0 deletions synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def read_config(self, config):
self.max_mau_value = config.get(
"max_mau_value", 0,
)
self.mau_stats_only = config.get("mau_stats_only", False)

self.mau_limits_reserved_threepids = config.get(
"mau_limit_reserved_threepids", []
Expand Down Expand Up @@ -372,6 +373,11 @@ def default_config(self, server_name, **kwargs):
# max_mau_value: 50
# mau_trial_days: 2
#
# If enabled, the metrics for the number of monthly active users will
# be populated, however no one will be limited. If limit_usage_by_mau
# is true, this is implied to be true.
# mau_stats_only: False
#
# Sometimes the server admin will want to ensure certain accounts are
# never blocked by mau checking. These accounts are specified here.
#
Expand Down
74 changes: 40 additions & 34 deletions synapse/storage/monthly_active_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,37 +96,38 @@ def _reap_users(txn):

txn.execute(sql, query_args)

# If MAU user count still exceeds the MAU threshold, then delete on
# a least recently active basis.
# Note it is not possible to write this query using OFFSET due to
# incompatibilities in how sqlite and postgres support the feature.
# sqlite requires 'LIMIT -1 OFFSET ?', the LIMIT must be present
# While Postgres does not require 'LIMIT', but also does not support
# negative LIMIT values. So there is no way to write it that both can
# support
safe_guard = self.hs.config.max_mau_value - len(self.reserved_users)
# Must be greater than zero for postgres
safe_guard = safe_guard if safe_guard > 0 else 0
query_args = [safe_guard]

base_sql = """
DELETE FROM monthly_active_users
WHERE user_id NOT IN (
SELECT user_id FROM monthly_active_users
ORDER BY timestamp DESC
LIMIT ?
if self.hs.config.limit_usage_by_mau:
# If MAU user count still exceeds the MAU threshold, then delete on
# a least recently active basis.
# Note it is not possible to write this query using OFFSET due to
# incompatibilities in how sqlite and postgres support the feature.
# sqlite requires 'LIMIT -1 OFFSET ?', the LIMIT must be present
# While Postgres does not require 'LIMIT', but also does not support
# negative LIMIT values. So there is no way to write it that both can
# support
safe_guard = self.hs.config.max_mau_value - len(self.reserved_users)
# Must be greater than zero for postgres
safe_guard = safe_guard if safe_guard > 0 else 0
query_args = [safe_guard]

base_sql = """
DELETE FROM monthly_active_users
WHERE user_id NOT IN (
SELECT user_id FROM monthly_active_users
ORDER BY timestamp DESC
LIMIT ?
)
"""
# Need if/else since 'AND user_id NOT IN ({})' fails on Postgres
# when len(reserved_users) == 0. Works fine on sqlite.
if len(self.reserved_users) > 0:
query_args.extend(self.reserved_users)
sql = base_sql + """ AND user_id NOT IN ({})""".format(
','.join(questionmarks)
)
"""
# Need if/else since 'AND user_id NOT IN ({})' fails on Postgres
# when len(reserved_users) == 0. Works fine on sqlite.
if len(self.reserved_users) > 0:
query_args.extend(self.reserved_users)
sql = base_sql + """ AND user_id NOT IN ({})""".format(
','.join(questionmarks)
)
else:
sql = base_sql
txn.execute(sql, query_args)
else:
sql = base_sql
txn.execute(sql, query_args)

yield self.runInteraction("reap_monthly_active_users", _reap_users)
# It seems poor to invalidate the whole cache, Postgres supports
Expand Down Expand Up @@ -252,8 +253,7 @@ def populate_monthly_active_users(self, user_id):
Args:
user_id(str): the user_id to query
"""

if self.hs.config.limit_usage_by_mau:
if self.hs.config.limit_usage_by_mau or self.hs.config.mau_stats_only:
# Trial users and guests should not be included as part of MAU group
is_guest = yield self.is_guest(user_id)
if is_guest:
Expand All @@ -271,8 +271,14 @@ def populate_monthly_active_users(self, user_id):
# but only update if we have not previously seen the user for
# LAST_SEEN_GRANULARITY ms
if last_seen_timestamp is None:
count = yield self.get_monthly_active_count()
if count < self.hs.config.max_mau_value:
# In the case where mau_stats_only is True and limit_usage_by_mau is
# False, there is no point in checking get_monthly_active_count - it
# adds no value and will break the logic if max_mau_value is exceeded.
if not self.hs.config.limit_usage_by_mau:
yield self.upsert_monthly_active_user(user_id)
else:
count = yield self.get_monthly_active_count()
if count < self.hs.config.max_mau_value:
yield self.upsert_monthly_active_user(user_id)
elif now - last_seen_timestamp > LAST_SEEN_GRANULARITY:
yield self.upsert_monthly_active_user(user_id)
25 changes: 25 additions & 0 deletions tests/storage/test_monthly_active_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,28 @@ def test_get_reserved_real_user_account(self):
self.store.user_add_threepid(user2, "email", user2_email, now, now)
count = self.store.get_registered_reserved_users_count()
self.assertEquals(self.get_success(count), len(threepids))

def test_track_monthly_users_without_cap(self):
self.hs.config.limit_usage_by_mau = False
self.hs.config.mau_stats_only = True
self.hs.config.max_mau_value = 1 # should not matter

count = self.store.get_monthly_active_count()
self.assertEqual(0, self.get_success(count))

self.store.upsert_monthly_active_user("@user1:server")
self.store.upsert_monthly_active_user("@user2:server")
self.pump()

count = self.store.get_monthly_active_count()
self.assertEqual(2, self.get_success(count))

def test_no_users_when_not_tracking(self):
self.hs.config.limit_usage_by_mau = False
self.hs.config.mau_stats_only = False
self.store.upsert_monthly_active_user = Mock()

self.store.populate_monthly_active_users("@user:sever")
self.pump()

self.store.upsert_monthly_active_user.assert_not_called()
18 changes: 18 additions & 0 deletions tests/test_mau.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,24 @@ def test_trial_users_cant_come_back(self):
self.assertEqual(e.code, 403)
self.assertEqual(e.errcode, Codes.RESOURCE_LIMIT_EXCEEDED)

def test_tracked_but_not_limited(self):
self.hs.config.max_mau_value = 1 # should not matter
self.hs.config.limit_usage_by_mau = False
self.hs.config.mau_stats_only = True

# Simply being able to create 2 users indicates that the
# limit was not reached.
token1 = self.create_user("kermit1")
self.do_sync_for_user(token1)
token2 = self.create_user("kermit2")
self.do_sync_for_user(token2)

# We do want to verify that the number of tracked users
# matches what we want though
count = self.store.get_monthly_active_count()
self.reactor.advance(100)
self.assertEqual(2, self.successResultOf(count))

def create_user(self, localpart):
request_data = json.dumps(
{
Expand Down
1 change: 1 addition & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def default_config(name):
config.hs_disabled_limit_type = ""
config.max_mau_value = 50
config.mau_trial_days = 0
config.mau_stats_only = False
config.mau_limits_reserved_threepids = []
config.admin_contact = None
config.rc_messages_per_second = 10000
Expand Down