Skip to content

Commit 559c769

Browse files
authored
Merge pull request #157 from WikiMovimentoBrasil/2025-01-improve-background-runners
feat: improve background runners and other stuff
2 parents 9d9c8de + 803458f commit 559c769

File tree

11 files changed

+74
-24
lines changed

11 files changed

+74
-24
lines changed

src/app.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ vacuum = true
1515
die-on-term = true
1616

1717
attach-daemon = python3 manage.py send_batches
18+
attach-daemon = python3 manage.py send_batches
19+
attach-daemon = python3 manage.py send_batches
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import time
2+
import logging
23

34
from django.core.management.base import BaseCommand
45
from core.models import Batch
6+
from django.db import transaction
7+
from django.db.utils import OperationalError
8+
9+
logger = logging.getLogger("qsts3")
510

611

712
class Command(BaseCommand):
@@ -13,20 +18,34 @@ def handle(self, *args, **options):
1318
self.send_start_message()
1419

1520
while True:
16-
batch = self.get_first_batch()
21+
batch = self.get_first_batch_and_lock()
1722
if batch:
1823
batch.run()
1924
else:
2025
time.sleep(self.TIMEOUT_SEC)
2126

2227
def send_start_message(self):
23-
msg = self.style.SUCCESS("==> send_batches management command started!")
24-
self.stdout.write(msg)
28+
logger.info("[command] send_batches management command started!")
29+
30+
def get_first_batch_and_lock(self):
31+
with transaction.atomic():
32+
try:
33+
batch = self.get_first_batch()
34+
self.lock_batch(batch)
35+
except OperationalError:
36+
logger.debug("[command] race condition mitigated")
37+
return None
38+
return batch
2539

2640
def get_first_batch(self):
2741
return (
28-
Batch.objects.select_for_update()
42+
Batch.objects.select_for_update(skip_locked=True)
2943
.filter(status=Batch.STATUS_INITIAL)
3044
.order_by("id")
3145
.first()
3246
)
47+
48+
def lock_batch(self, batch):
49+
if batch:
50+
batch.status = batch.STATUS_RUNNING
51+
batch.save()

src/core/models.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def run(self):
8585
Sends all the batch commands to the Wikidata API. This method should not fail.
8686
Sets the batch status to BLOCKED when a command fails.
8787
"""
88-
# Ignore when not INITIAL
89-
if not self.is_initial:
88+
# Ignore when not INITIAL or RUNNING
89+
if not self.is_initial_or_running:
9090
return
9191

9292
self.start()
@@ -146,10 +146,13 @@ def finish(self):
146146
self.save()
147147

148148
def stop(self):
149-
logger.debug(f"[{self}] stop...")
150-
self.message = f"Batch stopped processing by owner at {datetime.now()}"
151-
self.status = self.STATUS_STOPPED
152-
self.save()
149+
if not self.is_done:
150+
logger.debug(f"[{self}] stop...")
151+
self.message = f"Batch stopped processing by owner at {datetime.now()}"
152+
self.status = self.STATUS_STOPPED
153+
self.save()
154+
else:
155+
logger.debug(f"[{self}] user tried to stop but batch is done.")
153156

154157
def restart(self):
155158
if self.is_stopped:
@@ -386,7 +389,7 @@ class Error(models.TextChoices):
386389
)
387390

388391
def __str__(self):
389-
return f"Batch #{self.batch.pk} Command #{self.pk}"
392+
return f"Batch #{self.batch.pk} Command #{self.pk} ##{self.index}"
390393

391394
# -----------------
392395
# Status-changing methods
@@ -528,13 +531,21 @@ def parser_value_to_api_value(self, parser_value):
528531

529532
@property
530533
def statement_api_value(self):
534+
self.update_quantity_units_if_needed()
535+
return self.parser_value_to_api_value(self.json["value"])
536+
537+
def update_quantity_units_if_needed(self):
538+
# TODO: maybe we can add this elsewhere,
539+
# because `statement_api_value` above is called a lot
531540
value = self.json["value"]
532-
if value["type"] == "quantity" and value["value"]["unit"] != "1":
533-
base = self.batch.wikibase_url()
541+
base = self.batch.wikibase_url()
542+
if (value["type"] == "quantity"
543+
and value["value"]["unit"] != "1"
544+
and base not in value["value"]["unit"]
545+
):
534546
unit_id = value["value"]["unit"]
535547
full_unit = f"{base}/entity/Q{unit_id}"
536548
value["value"]["unit"] = full_unit
537-
return self.parser_value_to_api_value(value)
538549

539550
def update_statement(self, st):
540551
st.setdefault("property", {"id": self.prop})
@@ -787,7 +798,7 @@ def update_combining_state(self, client: Client):
787798
commands=commands,
788799
entity=entity,
789800
)
790-
logger.debug(f"[{self}] combined. final entity={entity}")
801+
logger.debug(f"[{self}] combined with next")
791802

792803
@property
793804
def final_combining_state(self):

src/qsts3/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
"version": 1,
142142
"disable_existing_loggers": False,
143143
"formatters": {
144-
"simple": {"format": "%(asctime)s [%(levelname)s] %(message)s"},
144+
"simple": {"format": "%(asctime)s [pid=%(process)d] [%(levelname)s] %(message)s"},
145145
"complete": {"format": "%(asctime)s - [%(levelname)s] %(name)s => %(message)s"},
146146
},
147147
"handlers": {

src/web/templates/batch.html

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,8 @@ <h2> {% translate "Batch" %} #{{ batch.pk }} <img id="spinner" class="htmx-indic
4141

4242
<div id="batchProgressDiv"
4343
hx-get="{% url 'batch_summary' pk=batch.pk %}"
44-
{% if batch.is_initial_or_running %}
45-
hx-trigger="load, every 3s"
46-
{% else %}
4744
hx-trigger="load"
48-
{% endif %}
49-
hx-swap="innerHTML"
45+
hx-swap="outerHTML"
5046
hx-indicator="#spinner"
5147
style="margin: 20px 0; font-size: 14px;">
5248
{% translate "Loading summary..." %}

src/web/templates/batch_summary.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
{% load i18n %}
22

3+
<div id="batchProgressDiv"
4+
{% if batch.is_initial_or_running %}
5+
hx-get="{% url 'batch_summary' pk=pk %}?previous_status={{ batch.status }}"
6+
hx-trigger="load delay:3s"
7+
hx-swap="outerHTML"
8+
hx-indicator="#spinner"
9+
{% endif %}
10+
style="margin: 20px 0; font-size: 14px;">
11+
312
<div class="progress" id="batchprogress">
413
<div class="progress-status">{% translate "BATCH STATUS" %}
514
<b class="status status_{{ status | lower }}">{{status | upper}}</b>
@@ -31,3 +40,5 @@
3140
</small>
3241
{% endif %}
3342
</div>
43+
44+
</div>

src/web/templates/preview_batch.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ <h2> {% translate "Batch Preview" %} <img id="spinner" class="htmx-indicator"></
3030
</p>
3131
</div>
3232
<div id="batchProgressDiv" style="margin: 20px 0; font-size: 14px; ">
33-
{% include 'batch_summary.html' with done_count=0 done_percentage=0 status="Preview" show_block_on_errors_notice=batch.block_on_errors %}
33+
{% include 'batch_summary.html' with batch=batch done_count=0 done_percentage=0 status="Preview" show_block_on_errors_notice=batch.block_on_errors %}
3434
</div>
3535
<h4>{% translate "Commands" %}</h4>
3636
<div class="overflow-auto" id="batchCommandsDiv" hx-get="{% url 'preview_batch_commands' %}" hx-trigger="load, reload" hx-indicator="#spinner" hx-swap="innerHTML">

src/web/tests/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ def test_batch_report(self, mocker):
598598
result = (
599599
"""b'batch_id,index,operation,status,error,message,entity_id,raw_input,api_response\\r\\n"""
600600
"""1,0,set_statement,Done,,,Q1234,Q1234|P2|Q1,{\\\'id\\\': \\\'Q1234$stuff\\\'}\\r\\n"""
601-
"""1,1,set_label,Done,,,Q11,"Q11|Len|""label"" ","""
601+
"""1,1,set_label,Done,,,Q11,"Q11|Len|""label""\","""
602602
""""{\\\'id\\\': \\\'Q11\\\', \\\'labels\\\': {\\\'en\\\': \\\'label\\\'}}"\\r\\n\'"""
603603
)
604604
self.assertEqual(result, str(response.content).strip())

src/web/views/batch.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,12 @@ def batch_summary(request, pk):
174174
)
175175
show_block_on_errors_notice = batch.is_preview_initial_or_running and batch.block_on_errors
176176

177-
return render(
177+
response = render(
178178
request,
179179
"batch_summary.html",
180180
{
181181
"pk": batch.pk,
182+
"batch": batch,
182183
"status": batch.get_status_display(),
183184
"error_count": batch.error_commands,
184185
"initial_count": batch.initial_commands,
@@ -191,5 +192,12 @@ def batch_summary(request, pk):
191192
"show_block_on_errors_notice": show_block_on_errors_notice,
192193
},
193194
)
195+
if batch.is_done:
196+
previous_status = request.GET.get("previous_status")
197+
if previous_status and previous_status != str(batch.STATUS_DONE):
198+
# Refreshing the page to load the correct buttons
199+
# and reload the command list if it's the first DONE
200+
response.headers["HX-Refresh"] = "true"
201+
return response
194202
except Batch.DoesNotExist:
195203
return render(request, "batch_summary.html", {}, status=404)

src/web/views/new_batch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def new_batch(request):
147147

148148
batch = parser.parse(batch_name, batch_owner, batch_commands)
149149

150+
batch.status = batch.STATUS_PREVIEW
150151
batch.block_on_errors = "block_on_errors" in request.POST
151152
batch.combine_commands = "do_not_combine_commands" not in request.POST
152153

0 commit comments

Comments
 (0)