Skip to content

Commit

Permalink
Add cron service and cleanup script (#167)
Browse files Browse the repository at this point in the history
* Add cron service and cleanup script

* Fix rockfile

* Fix restart_synapse()

* Remove unused fn

* Fix restart_synapse()

* Fix linting

* Fix rockfile

* Throttle cleanup operations

* Add missing headers

* Add some licensing exceptions

* Try to fix headers ignore once again

* Make scripts added to the rock executable

* Rerun CI

* Only modify rockcraft.yml where necessary

* Fix rockcraft scripts permissions

* Fix typo

---------

Co-authored-by: Amanda H. L. de Andrade Katz <amanda.katz@canonical.com>
Co-authored-by: arturo-seijas <102022572+arturo-seijas@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 8, 2024
1 parent 5ea89ab commit dbe44c4
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ header:
- 'zap_rules.tsv'
- 'lib/**'
- 'templates/**'
- 'synapse_rock/cron/**/*.py'
- 'synapse_rock/scripts/**/*.py'
comment: on-failure
25 changes: 25 additions & 0 deletions src/pebble.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def restart_synapse(self, container: ops.model.Container) -> None:
"""
logger.debug("Restarting the Synapse container")
container.add_layer(synapse.SYNAPSE_SERVICE_NAME, self._pebble_layer, combine=True)
container.add_layer(
synapse.SYNAPSE_CRON_SERVICE_NAME, self._cron_pebble_layer, combine=True
)
container.restart(synapse.SYNAPSE_SERVICE_NAME)

def replan_nginx(self, container: ops.model.Container) -> None:
Expand Down Expand Up @@ -253,3 +256,25 @@ def _mjolnir_pebble_layer(self) -> ops.pebble.LayerDict:
},
}
return typing.cast(ops.pebble.LayerDict, layer)

@property
def _cron_pebble_layer(self) -> ops.pebble.LayerDict:
"""Generate pebble config for the cron service.
Returns:
The pebble configuration for the cron service.
"""
layer = {
"summary": "Synapse cron layer",
"description": "Synapse cron layer",
"services": {
synapse.SYNAPSE_CRON_SERVICE_NAME: {
"override": "replace",
"summary": "Cron service",
"command": "/usr/local/bin/run_cron.py",
"environment": synapse.get_environment(self._charm_state),
"startup": "enabled",
},
},
}
return typing.cast(ops.pebble.LayerDict, layer)
1 change: 1 addition & 0 deletions src/synapse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
SYNAPSE_CONFIG_DIR,
SYNAPSE_CONFIG_PATH,
SYNAPSE_CONTAINER_NAME,
SYNAPSE_CRON_SERVICE_NAME,
SYNAPSE_DATA_DIR,
SYNAPSE_GROUP,
SYNAPSE_NGINX_CONTAINER_NAME,
Expand Down
1 change: 1 addition & 0 deletions src/synapse/workload.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
SYNAPSE_COMMAND_PATH = "/start.py"
SYNAPSE_CONFIG_PATH = f"{SYNAPSE_CONFIG_DIR}/homeserver.yaml"
SYNAPSE_CONTAINER_NAME = "synapse"
SYNAPSE_CRON_SERVICE_NAME = "synapse-cron"
SYNAPSE_DATA_DIR = "/data"
SYNAPSE_GROUP = "synapse"
SYNAPSE_NGINX_CONTAINER_NAME = "synapse-nginx"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""
Synapse does not purge empty directories from its media content storage locations.
Those can accumulate and eat up inodes and space.
Related: https://github.com/matrix-org/synapse/issues/16229
Related: https://github.com/matrix-org/synapse/issues/7690
"""

import os
import json
import time

# We assume that pyyaml is present thanks to synapse
import yaml

"""
To make sure that we don't steal IOPS from the running synapse instance,
we need to throttle the walk/rmdir iterations.
Given the MAX_IOPS, the formula is:
ITERATIONS_BEFORE_SLEEP = (TARGET_IOPS * SLEEP_TIME * MAX_IOPS)/(MAX_IOPS - TARGET_IOPS)
The values choosen here are for a TARGET_IOPS of 100 on a disk of 1600 MAX_IOPS.
Which is very conservative if using a SSD. Check the table at https://en.wikipedia.org/wiki/IOPS#Solid-state_devices
"""
ITERATIONS_BEFORE_SLEEP = 100
SLEEP_TIME = 1


# This function is meant to fail if the media_store_path can't be found
def load_media_store_path(file_path: str) -> str:
with open(file_path, "r", encoding="utf-8") as file:
data = yaml.safe_load(file)
return data["media_store_path"]


def delete_empty_dirs(path: str) -> None:
i = 0
for root, dirs, _ in os.walk(path, topdown=False):
i += 1
if i > ITERATIONS_BEFORE_SLEEP:
i = 0
time.sleep(SLEEP_TIME)
for dir in dirs:
try:
os.rmdir(os.path.join(root, dir))
except OSError:
continue


if __name__ == "__main__":
# load the environment from the file produced by /usr/local/bin/run_cron.py
if os.path.isfile("/var/local/cron/environment.json"):
with open("/var/local/cron/environment.json", "r") as env_fd:
env = json.load(env_fd)
if os.path.isfile(env["SYNAPSE_CONFIG_PATH"]):
path = load_media_store_path(env["SYNAPSE_CONFIG_PATH"])
if path:
delete_empty_dirs(path)
23 changes: 23 additions & 0 deletions synapse_rock/rockcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ license: Apache-2.0
platforms:
amd64:
parts:
scripts:
plugin: dump
source: scripts
organize:
"*": usr/local/bin/
override-prime: |
craftctl default
chmod -R +x usr/local/bin/*
cron:
after:
- scripts
stage-packages:
- cron
plugin: dump
source: cron
organize:
"cron.hourly/*": /etc/cron.hourly
"cron.daily/*": /etc/cron.daily
"cron.weekly/*": /etc/cron.weekly
"cron.monthly/*": /etc/cron.monthly
override-prime: |
craftctl default
chmod -R +x etc/cron.*
add-user:
plugin: nil
overlay-script: |
Expand Down
24 changes: 24 additions & 0 deletions synapse_rock/scripts/run_cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""
The goal of this script is to serialize the environment passed by pebble to a file
and then to start the cron service. We're doing that because the environment of the cron service
is not passed to the commands started by it.
An alternative would have been to write to /etc/profile and start the called cron scripts with a login shell.
But we wanted to use python scripts and not pollute the environment of the users
(Creating a specific user for the cron service would be an added complexity).
"""

import json
import os

if __name__ == "__main__":
file_path = "/var/local/cron/environment.json"
os.makedirs(os.path.dirname(file_path), exist_ok=True)

with open(file_path, "w") as file:
json.dump(dict(os.environ), file)

os.execv("/usr/sbin/cron", ["/usr/sbin/cron", "-f", "-P"])

0 comments on commit dbe44c4

Please sign in to comment.