Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c659164
feature ok / needs to be tested
IdirLISN Mar 31, 2026
d9bb30e
feature deactivated by default + hiden behind a button by default + v…
IdirLISN Apr 2, 2026
e0b875c
worker monitoring button behind user menu
IdirLISN Apr 2, 2026
77a18ba
files blacked for fixing the formatting issues
IdirLISN Apr 2, 2026
5081cdf
fixing synthax and format
IdirLISN Apr 2, 2026
c83ea33
rebase on dev
IdirLISN May 27, 2026
3143faf
clean feature
IdirLISN May 27, 2026
de8feae
git rebase continue
IdirLISN May 27, 2026
6f8f75d
feature in progress
IdirLISN Apr 7, 2026
b461f88
git rebase continue
IdirLISN May 27, 2026
5c6e8df
git rebase continue
IdirLISN May 27, 2026
62bfdea
compute worker monitoring on private queues (amazing stuff)
IdirLISN May 7, 2026
5940542
test number 245
IdirLISN May 7, 2026
15a3f20
git rebase continue
IdirLISN May 27, 2026
fa2f190
rebase and fix incoming
IdirLISN May 27, 2026
068ca60
rebase and fix incoming
IdirLISN May 27, 2026
d833028
feature clean
IdirLISN May 27, 2026
942e1c5
conflicts solved
IdirLISN May 28, 2026
37668f5
conflicts solved
IdirLISN May 28, 2026
353129b
private CW pb solved
IdirLISN May 28, 2026
63b7c65
linter fix
IdirLISN May 28, 2026
005058e
remove comment
IdirLISN May 28, 2026
8a81a35
feature en cours
IdirLISN Jun 1, 2026
667164f
monitor queues feature for admin ok
IdirLISN Jun 2, 2026
a580c30
UX/UI improved
IdirLISN Jun 2, 2026
98bb4e6
UI/UX improvment
IdirLISN Jun 4, 2026
cdd0610
final push
IdirLISN Jun 4, 2026
bab32c4
adding colors (final push)
IdirLISN Jun 4, 2026
14f62b1
panel resize
IdirLISN Jun 4, 2026
884563c
UI cleaning
IdirLISN Jun 4, 2026
78dc9be
save panel state
IdirLISN Jun 4, 2026
a2304a2
revert details.tag before CW monitoring merge
IdirLISN Jun 4, 2026
84952e5
worker jobs fix first try
IdirLISN Jun 4, 2026
3e1345c
adding queue jobs display
IdirLISN Jun 5, 2026
17783c5
queue jobs feaute ux/ui polish
IdirLISN Jun 8, 2026
6967094
improve ux with animation
IdirLISN Jun 9, 2026
085f6d6
worker count feature added
IdirLISN Jun 9, 2026
6f4cdaf
feature polish
IdirLISN Jun 9, 2026
754bf74
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
c024dc9
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
fb1c736
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
dae75d8
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
721a2ea
bugfix, queue stats + organizer panel ux fix
IdirLISN Jun 9, 2026
b7c19e4
queues stats dispay at top
IdirLISN Jun 16, 2026
7100d79
Feature group routing for submissions (#2393)
IdirLISN Jun 16, 2026
b8e14ec
site worker conflict solved
IdirLISN Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
634 changes: 335 additions & 299 deletions src/apps/competitions/tasks.py

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions src/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,6 @@
'task': 'profiles.tasks.clean_non_activated_users',
'schedule': timedelta(days=1), # Run every 24 hours
},
"refresh_compute_worker_health": {
"task": "competitions.tasks.refresh_compute_worker_health",
"schedule": 60,
},
}
CELERY_TIMEZONE = 'UTC'
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
Expand Down
2 changes: 0 additions & 2 deletions src/static/riot/competitions/detail/_header.tag
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@
Migrate
</button>

<!--
<worker-monitor-toggle
if="{competition.admin}"
can_view_workers_panel="true"
competition_id="{ competition.id }">
</worker-monitor-toggle>
-->

</div>
<div class="row">
Expand Down
679 changes: 48 additions & 631 deletions src/static/riot/competitions/detail/detail.tag

Large diffs are not rendered by default.

1,190 changes: 972 additions & 218 deletions src/static/riot/competitions/detail/worker-monitor-toggle.tag

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/templates/pages/monitor_queues.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,21 @@ <h1>Monitor queues</h1>
</div>
</div>
{% endif %}

{% if user.is_superuser %}
<div class="ui container">

<div class="ui segment">
<worker-monitor-toggle
can_view_workers_panel="true"
all_workers="true"
inline_mode="true">
</worker-monitor-toggle>
</div>

<div id="external_monitors" class="ui two column grid">
</div>
</div>
{% endif %}

{% endblock %}
122 changes: 47 additions & 75 deletions src/utils/consumers.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,20 @@
import asyncio
import json
import logging
import time

from competitions.models import Competition

from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django_redis import get_redis_connection

from utils.worker_utils import WORKER_HEARTBEAT_TTL, WORKERS_REGISTRY_KEY
from utils.worker_utils import fetch_compute_workers

logger = logging.getLogger(__name__)

r = get_redis_connection("default")


def _load_snapshot(competition_queue_name=None):
"""
Charge les workers depuis Redis.
- workers par défaut : toujours inclus (queue_source == 'default')
- workers privés : inclus uniquement si leur queue_source correspond
à la queue de la compétition courante
"""
raw = r.hgetall(WORKERS_REGISTRY_KEY)
workers = []
private_workers = []
now = time.time()

for _, value in raw.items():
try:
worker = json.loads(value)
except Exception:
continue

if now - worker.get("last_seen", 0) > WORKER_HEARTBEAT_TTL:
continue

if worker.get("queue_source") == "default":
workers.append(worker)
else:
# Worker privé : n'afficher que si la queue correspond à la compétition
if competition_queue_name and worker.get("queue_source") == competition_queue_name:
private_workers.append(worker)

workers.sort(key=lambda x: x.get("hostname", ""))
private_workers.sort(key=lambda x: (x.get("queue_source", ""), x.get("hostname", "")))
return workers, private_workers


def _get_competition_queue_name(competition_id):
"""Retourne le nom de la queue de la compétition, ou None."""
if not competition_id:
return None
try:
from competitions.models import Competition

competition = Competition.objects.select_related("queue").get(pk=competition_id)
if competition.queue and competition.queue.name:
return competition.queue.name
Expand All @@ -62,6 +23,30 @@ def _get_competition_queue_name(competition_id):
return None


def _load_snapshot(competition_queue_name=None, show_all=False):
workers, private_workers, queue_stats = fetch_compute_workers()

if show_all:
pass
elif competition_queue_name:
private_workers = [
w
for w in private_workers
if w.get("queue_source") == competition_queue_name
]
queue_stats = [
q
for q in queue_stats
if q.get("source_name") == competition_queue_name
or q.get("source_name") == "default"
]
else:
private_workers = []
queue_stats = [q for q in queue_stats if q.get("source_name") == "default"]

return workers, private_workers, queue_stats


class ComputeWorkersConsumer(AsyncJsonWebsocketConsumer):

async def connect(self):
Expand All @@ -72,6 +57,7 @@ async def connect(self):
await self.accept()
await self.channel_layer.group_add("compute_workers", self.channel_name)
self._competition_queue_name = None
self._show_all = False
self._running = True
self._subscribed = asyncio.Event()
self._task = asyncio.create_task(self._push_workers_loop())
Expand All @@ -88,55 +74,41 @@ async def disconnect(self, close_code):
pass

async def receive_json(self, content):
logger.debug("WebSocket received: %s", content)
if content.get("type") == "subscribe":
competition_id = content.get("competition_id")
self._competition_queue_name = await sync_to_async(_get_competition_queue_name)(
competition_id)
if content.get("all_workers"):
self._show_all = True
else:
competition_id = content.get("competition_id")
self._competition_queue_name = await sync_to_async(
_get_competition_queue_name
)(competition_id)
self._subscribed.set()

async def _push_workers_loop(self):
try:
try:
await asyncio.wait_for(self._subscribed.wait(), timeout=5.0)
except asyncio.TimeoutError:
logger.warning("WebSocket subscribe timeout, proceeding without competition filter")
logger.warning("WebSocket subscribe timeout, proceeding without filter")

while self._running:
workers, private_workers = await sync_to_async(_load_snapshot)(
self._competition_queue_name
workers, private_workers, queue_stats = await sync_to_async(_load_snapshot)(
competition_queue_name=self._competition_queue_name,
show_all=self._show_all,
)
if not self._running:
break
try:
await self.send_json({
"type": "workers.snapshot",
"workers": workers,
"private_workers": private_workers,
})
await self.send_json(
{
"type": "workers.snapshot",
"workers": workers,
"private_workers": private_workers,
"queue_stats": queue_stats,
}
)
except RuntimeError:
break
await asyncio.sleep(3)
except asyncio.CancelledError:
pass

async def worker_health(self, event):
worker = event["worker"]
is_default = worker.get("queue_source") == "default"
is_mine = (
self._competition_queue_name is not None
and worker.get("queue_source") == self._competition_queue_name
)
if not is_default and not is_mine:
return
try:
workers, private_workers = await sync_to_async(_load_snapshot)(
self._competition_queue_name
)
await self.send_json({
"type": "workers.snapshot",
"workers": workers,
"private_workers": private_workers,
})
except RuntimeError:
pass
Loading