applications: mark job as waiting when modules are still running (#89124)

This commit is contained in:
Lauréline Guérin 2024-04-12 18:17:10 +02:00
parent 25be449635
commit 422f55c2c5
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 59 additions and 44 deletions

View File

@ -544,6 +544,7 @@ class Version(models.Model):
STATUS_CHOICES = [
('registered', _('Registered')),
('running', _('Running')),
('waiting', _('Waiting for modules')),
('failed', _('Failed')),
('completed', _('Completed')),
]
@ -602,9 +603,16 @@ class AsyncJob(models.Model):
raise
finally:
if self.status == 'running':
self.status = 'completed'
if not self.progression_urls:
self.status = 'completed'
else:
self.status = 'waiting'
self.completion_timestamp = now()
self.save()
if self.status == 'waiting':
# the job is may be running in the same time that AsyncJobView is viewed
# the SQL transaction is not the same, run modules completion check now
self.check_modules_completion()
def get_diff_details(self):
# collect service bundle-check results
@ -659,3 +667,36 @@ class AsyncJob(models.Model):
if relation.element.slug in no_history.get(relation.element.type, []):
result_no_history.append(relation)
return result_diffs, result_not_found, result_no_history, result_legacy
def check_modules_completion(self):
if self.status != 'waiting':
return {}
context = {'job_progression': {}}
for service_id, services in self.progression_urls.items():
for service, url in services.items():
response = requests.get(url)
if not response.ok:
continue
context['job_progression'][service] = response.json()
context['job_progression'][service].update({'service_id': service_id})
context['services_all_completed'] = all(
[s['data']['status'] == 'completed' for s in context['job_progression'].values()]
)
failed = [
s['service_id'] for s in context['job_progression'].values() if s['data']['status'] == 'failed'
]
if failed:
self.status = 'failed'
if len(failed) > 1:
self.exception = 'Failed to deploy modules %s' % ', '.join(failed)
else:
self.exception = 'Failed to deploy module %s' % failed[0]
self.completion_timestamp = now()
self.save()
elif context['services_all_completed']:
self.status = 'completed'
self.completion_timestamp = now()
self.save()
context['wait_for_services'] = True
context['service_job_status_choices'] = {c[0]: c[1] for c in STATUS_CHOICES}
return context

View File

@ -28,7 +28,7 @@ from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.text import slugify
from django.utils.timezone import localtime, now
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, FormView, ListView, RedirectView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
@ -37,7 +37,6 @@ from hobo.environment.models import Variable
from .forms import GenerateForm, InstallForm, MetadataForm, VersionSelectForm
from .models import (
STATUS_CHOICES,
Application,
ApplicationError,
AsyncJob,
@ -673,32 +672,9 @@ class AsyncJobView(DetailView):
template_name = 'hobo/applications/job.html'
def get_context_data(self, **kwargs):
kwargs['job_progression'] = {}
job = self.object
if job.action == 'deploy' and job.status != 'failed':
for service_id, services in job.progression_urls.items():
for service, url in services.items():
response = requests.get(url)
if not response.ok:
continue
kwargs['job_progression'][service] = response.json()
kwargs['job_progression'][service].update({'service_id': service_id})
kwargs['services_all_completed'] = all(
[s['data']['status'] == 'completed' for s in kwargs['job_progression'].values()]
)
failed = [
s['service_id'] for s in kwargs['job_progression'].values() if s['data']['status'] == 'failed'
]
if failed:
job.status = 'failed'
if len(failed) > 1:
job.exception = 'Failed to deploy modules %s' % ', '.join(failed)
else:
job.exception = 'Failed to deploy module %s' % failed[0]
job.completion_timestamp = now()
job.save()
kwargs['wait_for_services'] = True
kwargs['service_job_status_choices'] = {c[0]: c[1] for c in STATUS_CHOICES}
kwargs['app'] = job.application
kwargs.update(job.check_modules_completion())
return super().get_context_data(**kwargs)
def get_redirect_url(self):

View File

@ -233,6 +233,11 @@ def mocked_http(url, request):
'content': json.dumps({'err': 0, 'url': 'https://wcs.example.invalid/api/jobs/job-uuid/'}),
'status_code': 200,
}
if url.path.startswith('/api/jobs/'):
return {
'content': json.dumps({'err': 0, 'data': {'status': 'completed', 'completion_status': '100%'}}),
'status_code': 200,
}
if url.path == '/api/export-import/bundle-check/':
return {
@ -1675,13 +1680,13 @@ def test_deploy_application_roles(app, admin_user, settings, app_bundle_roles):
)
# roles
assert mocked_http.call['requests'][1].url.startswith(
'https://idp.example.invalid/api/roles/?update'
'https://idp.example.invalid/api/roles/?update_or_create'
)
assert mocked_http.call['requests'][2].url.startswith(
'https://idp.example.invalid/api/provision/'
)
assert mocked_http.call['requests'][3].url.startswith(
'https://idp.example.invalid/api/roles/?update'
'https://idp.example.invalid/api/roles/?update_or_create'
)
assert mocked_http.call['requests'][4].url.startswith(
'https://idp.example.invalid/api/provision/'
@ -1695,11 +1700,14 @@ def test_deploy_application_roles(app, admin_user, settings, app_bundle_roles):
)
# then element refresh
available_objects = [o for o in WCS_AVAILABLE_OBJECTS['data'] if o['id'] in ['forms', 'roles']]
assert mocked_http.call['count'] == 6 + 1 + len(available_objects)
assert mocked_http.call['count'] == 6 + 1 + len(available_objects) + 1
for i, object_type in enumerate(available_objects):
assert mocked_http.call['requests'][7 + i].url.startswith(
'https://wcs.example.invalid/api/export-import/%s/?' % object_type['id']
)
assert mocked_http.call['requests'][9].url.startswith(
'https://wcs.example.invalid/api/jobs/job-uuid/'
)
resp = resp.follow()
assert resp.pyquery('h2.application-title').text() == 'Test'
@ -1819,7 +1827,7 @@ def test_job_status_page(app, admin_user, settings):
application=application,
action='deploy',
progression_urls={'wcs': {'Foobar': 'https://wcs.example.invalid/api/jobs/job-uuid/'}},
status='completed', # completed, with async job on wcs part
status='waiting',
)
def response_content(url, request):
@ -1837,17 +1845,7 @@ def test_job_status_page(app, admin_user, settings):
assert 'window.location.reload' in resp
assert 'window.location = "/applications/manifest/test/"' not in resp
def response_content(url, request): # noqa pylint: disable=function-redefined
if url.path.startswith('/api/jobs/'):
return {
'content': json.dumps(
{'err': 0, 'data': {'status': 'completed', 'completion_status': '100%'}}
),
'status_code': 200,
}
return mocked_http(url, request)
with StatefulHTTMock(response_content):
with StatefulHTTMock(httmock.remember_called(mocked_http)):
resp = app.get('/applications/manifest/test/job/%s/' % job.pk)
assert 'Please wait…' in resp
assert 'Completed: 100%' in resp
@ -1863,7 +1861,7 @@ def test_job_status_page(app, admin_user, settings):
assert 'window.location.reload' not in resp
assert 'window.location = "/applications/manifest/test/"' not in resp
job.status = 'running'
job.status = 'waiting'
job.exception = ''
job.save()