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
3cd3503
feature ok / needs to be tested
IdirLISN Mar 31, 2026
3fa38ca
feature deactivated by default + hiden behind a button by default + v…
IdirLISN Apr 2, 2026
e3ed9cd
worker monitoring button behind user menu
IdirLISN Apr 2, 2026
4888244
files blacked for fixing the formatting issues
IdirLISN Apr 2, 2026
9fcd852
fixing synthax and format
IdirLISN Apr 2, 2026
06e4ea1
rebase on dev
IdirLISN May 27, 2026
d78dbdc
clean feature
IdirLISN May 27, 2026
1e73950
git rebase continue
IdirLISN May 27, 2026
a8162de
feature in progress
IdirLISN Apr 7, 2026
2702d66
git rebase continue
IdirLISN May 27, 2026
733df04
git rebase continue
IdirLISN May 27, 2026
7e523e5
compute worker monitoring on private queues (amazing stuff)
IdirLISN May 7, 2026
829c0ed
test number 245
IdirLISN May 7, 2026
fbfcf18
git rebase continue
IdirLISN May 27, 2026
34a27dd
rebase and fix incoming
IdirLISN May 27, 2026
f00d169
rebase and fix incoming
IdirLISN May 27, 2026
4fa2acc
feature clean
IdirLISN May 27, 2026
35615ae
conflicts solved
IdirLISN May 28, 2026
7b67160
conflicts solved
IdirLISN May 28, 2026
5427d9e
private CW pb solved
IdirLISN May 28, 2026
b622af7
linter fix
IdirLISN May 28, 2026
a19e813
remove comment
IdirLISN May 28, 2026
45a415a
feature en cours
IdirLISN Jun 1, 2026
1bdeb83
monitor queues feature for admin ok
IdirLISN Jun 2, 2026
6d9f432
UX/UI improved
IdirLISN Jun 2, 2026
d387a55
UI/UX improvment
IdirLISN Jun 4, 2026
1b0556a
final push
IdirLISN Jun 4, 2026
4a11b99
adding colors (final push)
IdirLISN Jun 4, 2026
04650d6
panel resize
IdirLISN Jun 4, 2026
b05a1b6
UI cleaning
IdirLISN Jun 4, 2026
4c4ff14
save panel state
IdirLISN Jun 4, 2026
92ca75c
revert details.tag before CW monitoring merge
IdirLISN Jun 4, 2026
aac16ea
worker jobs fix first try
IdirLISN Jun 4, 2026
b4f9fff
adding queue jobs display
IdirLISN Jun 5, 2026
8edb80a
queue jobs feaute ux/ui polish
IdirLISN Jun 8, 2026
e63f832
improve ux with animation
IdirLISN Jun 9, 2026
1ed4f98
worker count feature added
IdirLISN Jun 9, 2026
345e7a0
feature polish
IdirLISN Jun 9, 2026
7482e12
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
36dd3b8
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
eeaf957
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
f1c3443
bugfix, status on CW instead of queue
IdirLISN Jun 9, 2026
32a1660
bugfix, queue stats + organizer panel ux fix
IdirLISN Jun 9, 2026
737017b
queues stats dispay at top
IdirLISN Jun 16, 2026
8e0c144
Feature group routing for submissions (#2393)
IdirLISN Jun 16, 2026
d9414f1
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