Skip to content

Commit 9d375b4

Browse files
authored
Limit number of diffs per worker process in server (#210)
This adds a new `MAX_DIFFS_PER_WORKER` environment variable that limits the number of diffs performed by a single worker. After a worker process performs this many diffs, it is shut down and replaced with a fresh process. This is an ugly first cut and needs a lot of cleaning up. It also doesn't really account for things per worker -- it just restarts the pool after DIFFER_PARALLELISM * MAX_DIFFS_PER_WORKER diffs. But newer versions of Python have an API that does this right and which we should eventually switch to when compatible. Fixes #202.
1 parent 1783275 commit 9d375b4

4 files changed

Lines changed: 54 additions & 2 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ export DIFFER_MAX_BODY_SIZE='10485760' # 10 MB
3434
# Set how many diffs can be run in parallel.
3535
# export DIFFER_PARALLELISM=10
3636

37+
# Once each worker process in the diff pool has done this many diffs, terminate
38+
# it and start a new worker. If 0 or unset, there is no limit.
39+
# (Think about this as restarting the worker pool after
40+
# MAX_DIFFS_PER_WORKER * DIFFER_PARALLELISM diffs)
41+
# export MAX_DIFFS_PER_WORKER=10
42+
3743
# Instead of crashing when the process pool used for running diffs breaks,
3844
# keep accepting requests and try to restart the pool.
3945
# RESTART_BROKEN_DIFFER='true'

docs/source/release-history.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ In Development
77

88
- Fix XML prolog detection in diff server. This could occasionally have inferred character encoding in an XML document that was inaccurate. (:issue:`209`)
99

10+
- Add ``MAX_DIFFS_PER_WORKER`` environment variable for diff server configuration. When set to a positive integer, a worker process that handles running the actual diff will be restarted after running this many diffs (the number of workers can be controlled with ``DIFFER_PARALLELISM``, which is not new). If ``0`` or not set, workers will only be restarted if they crash. Setting this appropriately can help keep resources within limits and prevent eventual hangs or crashes. (:issue:`210`)
11+
1012

1113
Version 0.1.7 (2025-10-06)
1214
--------------------------

web_monitoring_diff/server/server.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
sentry_sdk.integrations.logging.ignore_logger('tornado.access')
3737

3838
DIFFER_PARALLELISM = int(os.environ.get('DIFFER_PARALLELISM', 10))
39+
MAX_DIFFS_PER_WORKER = max(int(os.environ.get('MAX_DIFFS_PER_WORKER', 0)), 0)
3940
RESTART_BROKEN_DIFFER = os.environ.get('RESTART_BROKEN_DIFFER', 'False').strip().lower() == 'true'
4041

4142
# Map tokens in the REST API to functions in modules.
@@ -509,10 +510,18 @@ async def diff(self, func, a, b, params, tries=2):
509510
Actually do a diff between two pieces of content, optionally retrying
510511
if the process pool that executes the diff breaks.
511512
"""
512-
executor = self.get_diff_executor()
513+
reset = False
514+
if MAX_DIFFS_PER_WORKER and self.settings.get('remaining_diffs_for_executor', 0) <= 0:
515+
reset = True
516+
self.settings['remaining_diffs_for_executor'] = MAX_DIFFS_PER_WORKER * DIFFER_PARALLELISM
517+
executor = self.get_diff_executor(reset=reset)
518+
519+
# executor = self.get_diff_executor()
513520
loop = asyncio.get_running_loop()
514521
for attempt in range(tries):
515522
try:
523+
if MAX_DIFFS_PER_WORKER:
524+
self.settings['remaining_diffs_for_executor'] -= 1
516525
return await loop.run_in_executor(
517526
executor, functools.partial(caller, func, a, b, **params))
518527
except concurrent.futures.process.BrokenProcessPool:
@@ -522,7 +531,14 @@ async def diff(self, func, a, b, params, tries=2):
522531
# parallel diffs haven't already done it. If it's already
523532
# been reset, then we can just go and use the new one.
524533
old_executor, executor = executor, self.get_diff_executor()
525-
if executor == old_executor:
534+
if (
535+
executor == old_executor or
536+
(
537+
MAX_DIFFS_PER_WORKER and
538+
self.settings.get('remaining_diffs_for_executor', 0) <= 0
539+
)
540+
):
541+
self.settings['remaining_diffs_for_executor'] = MAX_DIFFS_PER_WORKER * DIFFER_PARALLELISM
526542
executor = self.get_diff_executor(reset=True)
527543
else:
528544
# If we shouldn't allow the server to keep rebuilding the

web_monitoring_diff/tests/test_server_exc_handling.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,34 @@ def get_executor(self, reset=False):
558558
assert not mock_quit.called
559559

560560

561+
@patch('web_monitoring_diff.server.server.DIFFER_PARALLELISM', 2)
562+
@patch('web_monitoring_diff.server.server.MAX_DIFFS_PER_WORKER', 2)
563+
@tornado.testing.gen_test
564+
async def test_max_diffs_per_worker(self):
565+
# The executor is created lazily, so do one request to create it.
566+
response = await self.fetch_async('/html_source_dmp?format=json&'
567+
f'a=file://{fixture_path("empty.txt")}&'
568+
f'b=file://{fixture_path("empty.txt")}')
569+
assert response.code == 200
570+
original_executor = self._app.settings.get('diff_executor')
571+
572+
# Make more than the max number of requests before restarting the diff
573+
# executor. This needs to be done in parallel so we can make sure
574+
# in-progress diffs don't get lost when rebuilding the executor.
575+
requests = [
576+
self.fetch_async('/html_source_dmp?format=json&'
577+
f'a=file://{fixture_path("empty.txt")}&'
578+
f'b=file://{fixture_path("empty.txt")}')
579+
for _i in range(4)
580+
]
581+
responses = await asyncio.gather(*requests)
582+
for response in responses:
583+
assert response.code == 200
584+
585+
executor = self._app.settings.get('diff_executor')
586+
assert original_executor is not executor
587+
588+
561589
def mock_diffing_method(c_body):
562590
return
563591

0 commit comments

Comments
 (0)