Skip to content

Commit

Permalink
Allow configuring a range for the account validity startup job
Browse files Browse the repository at this point in the history
When enabling the account validity feature, Synapse will look at startup for registered account without an expiration date, and will set one equals to 'now + validity_period' for them. On large servers, it can mean that a large number of users will have the same expiration date, which means that they will all be sent a renewal email at the same time, which isn't ideal.
In order to mitigate this, this PR allows server admins to define a 'max_delta' so that the expiration date is a random value in the [now + validity_period ; now + validity_period + max_delta] range. This allows renewal emails to be progressively sent over a configured period instead of being sent all in one big batch.
  • Loading branch information
babolivier committed May 28, 2019
1 parent ddd30f4 commit 5283988
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 2 deletions.
11 changes: 11 additions & 0 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def __init__(self, config, synapse_config):
else:
self.renew_email_subject = "Renew your %(app)s account"

self.startup_job_max_delta = self.parse_duration(
config.get("startup_job_max_delta", 0),
)

if self.renew_by_email_enabled and "public_baseurl" not in synapse_config:
raise ConfigError("Can't send renewal emails without 'public_baseurl'")

Expand Down Expand Up @@ -131,11 +135,18 @@ def default_config(self, generate_secrets=False, **kwargs):
# after that the validity period changes and Synapse is restarted, the users'
# expiration dates won't be updated unless their account is manually renewed.
#
# If set, the ``startup_job_max_delta`` optional setting will make the startup job
# described above set a random expiration date between t + period and
# t + period + startup_job_max_delta, t being the date and time at which the job
# sets the expiration date for a given user. This is useful for server admins that
# want to avoid Synapse sending a lot of renewal emails at once.
#
#account_validity:
# enabled: True
# period: 6w
# renew_at: 1w
# renew_email_subject: "Renew your %%(app)s account"
# startup_job_max_delta: 2d
# The user must provide all of the below types of 3PID when registering.
#
Expand Down
23 changes: 21 additions & 2 deletions synapse/storage/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# limitations under the License.
import itertools
import logging
import random
import sys
import threading
import time
Expand Down Expand Up @@ -247,6 +248,8 @@ def __init__(self, db_conn, hs):
self._check_safe_to_upsert,
)

self.rand = random.SystemRandom()

if self._account_validity.enabled:
self._clock.call_later(
0.0,
Expand Down Expand Up @@ -308,21 +311,37 @@ def select_users_with_no_expiration_date_txn(txn):
res = self.cursor_to_dict(txn)
if res:
for user in res:
self.set_expiration_date_for_user_txn(txn, user["name"])
self.set_expiration_date_for_user_txn(
txn,
user["name"],
use_delta=True,
)

yield self.runInteraction(
"get_users_with_no_expiration_date",
select_users_with_no_expiration_date_txn,
)

def set_expiration_date_for_user_txn(self, txn, user_id):
def set_expiration_date_for_user_txn(self, txn, user_id, use_delta=False):
"""Sets an expiration date to the account with the given user ID.
Args:
user_id (str): User ID to set an expiration date for.
use_delta (bool): If set to False, the expiration date for the user will be
now + validity period. If set to True, this expiration date will be a
random value in the [now + period; now + period + max_delta] range,
max_delta being the configured value for the size of the range, unless
delta is 0, in which case it sets it to now + period.
"""
now_ms = self._clock.time_msec()
expiration_ts = now_ms + self._account_validity.period

if use_delta and self._account_validity.startup_job_max_delta:
expiration_ts = self.rand.randrange(
expiration_ts,
expiration_ts + self._account_validity.startup_job_max_delta,
)

self._simple_insert_txn(
txn,
"account_validity",
Expand Down
21 changes: 21 additions & 0 deletions tests/rest/client/v2_alpha/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ class AccountValidityBackgroundJobTestCase(unittest.HomeserverTestCase):

def make_homeserver(self, reactor, clock):
self.validity_period = 10
self.max_delta = 10

config = self.default_config()

Expand All @@ -459,8 +460,28 @@ def test_background_job(self):
"""
user_id = self.register_user("kermit", "user")

self.hs.config.account_validity.startup_job_max_delta = 0

now_ms = self.hs.clock.time_msec()
self.get_success(self.store._set_expiration_date_when_missing())

res = self.get_success(self.store.get_expiration_ts_for_user(user_id))
self.assertEqual(res, now_ms + self.validity_period)

def test_background_job_with_max_delta(self):
"""
Tests the same thing as test_background_job, except that it sets the
startup_job_max_delta parameter and checks that the expiration date is within the
allowed range.
"""
user_id = self.register_user("kermit_delta", "user")

self.hs.config.account_validity.startup_job_max_delta = self.max_delta

now_ms = self.hs.clock.time_msec()
self.get_success(self.store._set_expiration_date_when_missing())

res = self.get_success(self.store.get_expiration_ts_for_user(user_id))

self.assertLessEqual(res, now_ms + self.validity_period + self.delta)
self.assertGreaterEqual(res, now_ms + self.validity_period)

0 comments on commit 5283988

Please sign in to comment.