Skip to content

Commit

Permalink
Merge branch 'master' into support-py312
Browse files Browse the repository at this point in the history
  • Loading branch information
fmigneault authored Nov 23, 2023
2 parents a6254ce + 4ea6f53 commit 9b76b22
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 44 deletions.
15 changes: 15 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,26 @@ Changes:
* Depends on ``pyramid_celery==5.0.0a`` [`crim-ca/pyramid_celery <https://github.com/crim-ca/pyramid_celery>`_ fork]
(relates to `sontek/pyramid_celery#102 <https://github.com/sontek/pyramid_celery/pull/102>`_).

Fixes:
------
- No change.

.. _changes_4.37.0:

`4.37.0 <https://github.com/crim-ca/weaver/tree/4.37.0>`_ (2023-11-22)
========================================================================

Changes:
--------
- No change.

Fixes:
------
- Fix default `XML` format resolution for `WPS` endpoint when no ``Accept`` header or ``format``/``f`` query parameter
is provided and that the request is submitted from a Web Browser, which involves additional control logic to select
the applicable ``Content-Type`` for the response.
- Fix pre-forked ``celery`` worker process inconsistently resolving the ``pyramid`` registry applied
by ``pyramid_celery`` after worker restart.

.. _changes_4.36.0:

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ MAKEFILE_NAME := $(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
# Application
APP_ROOT := $(abspath $(lastword $(MAKEFILE_NAME))/..)
APP_NAME := $(shell basename $(APP_ROOT))
APP_VERSION ?= 4.36.0
APP_VERSION ?= 4.37.0
APP_INI ?= $(APP_ROOT)/config/$(APP_NAME).ini
DOCKER_REPO ?= pavics/weaver
#DOCKER_REPO ?= docker-registry.crim.ca/ogc/weaver
Expand Down
20 changes: 10 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ for each process.
:alt: Requires Python 3.7+
:target: https://www.python.org/getit

.. |commits-since| image:: https://img.shields.io/github/commits-since/crim-ca/weaver/4.36.0.svg
.. |commits-since| image:: https://img.shields.io/github/commits-since/crim-ca/weaver/4.37.0.svg
:alt: Commits since latest release
:target: https://github.com/crim-ca/weaver/compare/4.36.0...master
:target: https://github.com/crim-ca/weaver/compare/4.37.0...master

.. |version| image:: https://img.shields.io/badge/latest%20version-4.36.0-blue
.. |version| image:: https://img.shields.io/badge/latest%20version-4.37.0-blue
:alt: Latest Tagged Version
:target: https://github.com/crim-ca/weaver/tree/4.36.0
:target: https://github.com/crim-ca/weaver/tree/4.37.0

.. |deps| image:: https://img.shields.io/librariesio/github/crim-ca/weaver
:alt: Libraries.io Dependencies Status
Expand All @@ -65,9 +65,9 @@ for each process.
:alt: Github Actions CI Build Status (master branch)
:target: https://github.com/crim-ca/weaver/actions?query=workflow%3ATests+branch%3Amaster

.. |github_tagged| image:: https://img.shields.io/github/actions/workflow/status/crim-ca/weaver/tests.yml?label=4.36.0&branch=4.36.0
.. |github_tagged| image:: https://img.shields.io/github/actions/workflow/status/crim-ca/weaver/tests.yml?label=4.37.0&branch=4.37.0
:alt: Github Actions CI Build Status (latest tag)
:target: https://github.com/crim-ca/weaver/actions?query=workflow%3ATests+branch%3A4.36.0
:target: https://github.com/crim-ca/weaver/actions?query=workflow%3ATests+branch%3A4.37.0

.. |readthedocs| image:: https://img.shields.io/readthedocs/pavics-weaver
:alt: ReadTheDocs Build Status (master branch)
Expand All @@ -79,7 +79,7 @@ for each process.

.. below shield will either indicate the targeted version or 'tag not found'
.. since docker tags are pushed following manual builds by CI, they are not automatic and no build artifact exists
.. |docker_build_status| image:: https://img.shields.io/docker/v/pavics/weaver/4.36.0?label=tag%20status
.. |docker_build_status| image:: https://img.shields.io/docker/v/pavics/weaver/4.37.0?label=tag%20status
:alt: Docker Build Status (latest version)
:target: https://hub.docker.com/r/pavics/weaver/tags

Expand Down Expand Up @@ -202,12 +202,12 @@ Docker image repositories:

::

$ docker pull pavics/weaver:4.36.0
$ docker pull pavics/weaver:4.37.0

For convenience, following tags are also available:

- ``weaver:4.36.0-manager``: `Weaver` image that will run the API for WPS process and job management.
- ``weaver:4.36.0-worker``: `Weaver` image that will run the process job runner application.
- ``weaver:4.37.0-manager``: `Weaver` image that will run the API for WPS process and job management.
- ``weaver:4.37.0-worker``: `Weaver` image that will run the process job runner application.

Following links correspond to existing servers with `Weaver` configured as *EMS*/*ADES* instances respectively.

Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile-base
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ LABEL description.short="Weaver Base"
LABEL description.long="Workflow Execution Management Service (EMS); Application, Deployment and Execution Service (ADES)"
LABEL maintainer="Francis Charette-Migneault <[email protected]>"
LABEL vendor="CRIM"
LABEL version="4.36.0"
LABEL version="4.37.0"

# setup paths
ENV APP_DIR=/opt/local/src/weaver
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.36.0
current_version = 4.37.0
commit = True
tag = True
tag_name = {new_version}
Expand Down
187 changes: 187 additions & 0 deletions tests/functional/test_celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""
Tests to validate that :mod:`celery` execution behaves as intended.
"""
import contextlib
import inspect
import json
import os
import subprocess
import sys
import tempfile
from typing import TYPE_CHECKING

from tests.utils import get_settings_from_testapp, get_test_weaver_app, setup_config_with_mongodb
from weaver.config import WeaverConfiguration
from weaver.database import get_db
from weaver.database.mongodb import get_mongodb_connection
from weaver.utils import retry_on_condition
from weaver.wps.utils import get_wps_url

if TYPE_CHECKING:
from pymongo.collection import Collection


def is_attribute_none(exception):
# type: (Exception) -> bool
return isinstance(exception, AttributeError) and "None" in str(exception)


def get_taskmeta_output(taskmeta_collection, output):
# type: (Collection, str) -> str
taskmeta = taskmeta_collection.find_one({"_id": output.strip()})
return taskmeta.get("traceback", "") + taskmeta.get("result", "")


def test_celery_registry_resolution():
python_bin = sys.executable
python_dir = os.path.dirname(python_bin)
debug_path = os.path.expandvars(os.environ["PATH"])
celery_bin = os.path.join(python_dir, "celery")

config = setup_config_with_mongodb(settings={
"weaver.configuration": WeaverConfiguration.HYBRID,
"weaver.wps_output_url": "http://localhost/wps-outputs",
"weaver.wps_output_dir": "/tmp/weaver-test/wps-outputs", # nosec: B108 # don't care hardcoded for test
})
webapp = get_test_weaver_app(config=config)
settings = get_settings_from_testapp(webapp)
wps_url = get_wps_url(settings)
job_store = get_db(settings).get_store("jobs")
job1 = job_store.save_job(
task_id="tmp",
process="jsonarray2netcdf",
inputs={"input": {"href": "http://random-dont-care.com/fake.json"}},
)
job2 = job_store.save_job(
task_id="tmp",
process="jsonarray2netcdf",
inputs={"input": {"href": "http://random-dont-care.com/fake.json"}},
)

with contextlib.ExitStack() as stack:
celery_mongo_broker = f"""mongodb://{settings["mongodb.host"]}:{settings["mongodb.port"]}/celery-test"""
cfg_ini = stack.enter_context(tempfile.NamedTemporaryFile(suffix=".ini", mode="w", encoding="utf-8"))
cfg_ini.write(
inspect.cleandoc(f"""
[app:main]
use = egg:weaver
[celery]
broker_url = {celery_mongo_broker}
result_backend = {celery_mongo_broker}
""")
)
cfg_ini.flush()
cfg_ini.seek(0)

celery_process = stack.enter_context(subprocess.Popen(
[
celery_bin,
"-A",
"pyramid_celery.celery_app",
"worker",
"-B",
"-E",
"--ini", cfg_ini.name,
"--loglevel=DEBUG",
"--time-limit", "10",
"--soft-time-limit", "10",
"--detach",
# following will cause an error on any subsequent task
# if registry is not properly retrieved across processes/threads
"--concurrency", "1",
"--max-tasks-per-child", "1",
],
universal_newlines=True,
start_new_session=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={"PATH": f"{python_dir}:{debug_path}"},
)) # type: subprocess.Popen
celery_stdout, celery_stderr = celery_process.communicate()
celery_output = celery_stdout + celery_stderr
assert "Traceback" not in celery_output, "Unhandled error at Weaver/Celery startup. Cannot resume test."
assert all([
msg in celery_output
for msg in
[
"Initiating weaver application",
"Celery runner detected.",
]
])

celery_task_cmd1 = stack.enter_context(subprocess.Popen(
[
celery_bin,
"-b", celery_mongo_broker,
"call",
"-a", json.dumps([str(job1.uuid), wps_url]),
"weaver.processes.execution.execute_process",
],
universal_newlines=True,
start_new_session=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={"PATH": f"{python_dir}:{debug_path}"},
)) # type: subprocess.Popen
celery_task_cmd2 = stack.enter_context(subprocess.Popen(
[
celery_bin,
"-b", celery_mongo_broker,
"call",
"-a", json.dumps([str(job2.uuid), wps_url]),
"weaver.processes.execution.execute_process",
],
universal_newlines=True,
start_new_session=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={"PATH": f"{python_dir}:{debug_path}"},
)) # type: subprocess.Popen

task1_output, _ = retry_on_condition(
lambda: celery_task_cmd1.communicate(),
condition=is_attribute_none, retries=5, interval=1,
)
task2_output, _ = retry_on_condition(
lambda: celery_task_cmd2.communicate(),
condition=is_attribute_none, retries=5, interval=1,
)

celery_mongo_db = get_mongodb_connection({
"mongodb.host": settings["mongodb.host"],
"mongodb.port": settings["mongodb.port"],
"mongodb.db_name": "celery-test",
})
celery_taskmeta = celery_mongo_db.celery_taskmeta
task1_result = retry_on_condition(
get_taskmeta_output, celery_taskmeta, task1_output,
condition=is_attribute_none, retries=5, interval=1,
)
task2_result = retry_on_condition(
get_taskmeta_output, celery_taskmeta, task2_output,
condition=is_attribute_none, retries=5, interval=1,
)

# following errors are not necessarily linked directly to celery failing
# however, if all other tests pass except this one, there's a big chance
# it is caused by a celery concurrency/processes/threading issue with the pyramid registry
potential_errors = [
"AttributeError: 'NoneType' object",
"if settings.get(setting, None) is None",
"get_registry()",
"get_settings()",
"get_db()",
"get_registry(app)",
"get_settings(app)",
"get_db(app)",
"get_registry(celery_app)",
"get_settings(celery_app)",
"get_db(celery_app)",
"get_registry(None)",
"get_settings(None)",
"get_db(None)",
]
task1_found_errors = [err_msg for err_msg in potential_errors if err_msg in task1_result]
task2_found_errors = [err_msg for err_msg in potential_errors if err_msg in task2_result]
assert not task1_found_errors, "potential error detected with celery and pyramid registry utilities"
assert not task2_found_errors, "potential error detected with celery and pyramid registry utilities"
14 changes: 9 additions & 5 deletions tests/functional/test_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
get_settings_from_config_ini,
get_settings_from_testapp,
get_test_weaver_app,
mocked_dismiss_process,
mocked_execute_celery,
mocked_file_server,
mocked_sub_requests,
Expand Down Expand Up @@ -451,10 +452,13 @@ def clean_test_processes(cls, allowed_codes=frozenset([HTTPOk.code, HTTPNotFound
headers=cls.headers, cookies=cls.cookies,
ignore_errors=True, log_enabled=False)
cls.assert_response(resp, allowed_codes, message="Failed cleanup of test processes jobs!")
for job in resp.json.get("jobs", []):
cls.request("DELETE", f"{path}/{job}",
headers=cls.headers, cookies=cls.cookies,
ignore_errors=True, log_enabled=False)
with contextlib.ExitStack() as stack:
if cls.is_webtest():
stack.enter_context(mocked_dismiss_process())
for job in resp.json.get("jobs", []):
cls.request("DELETE", f"{path}/{job}",
headers=cls.headers, cookies=cls.cookies,
ignore_errors=True, log_enabled=False)

# then clean the actual process
path = f"/processes/{process_info.id}"
Expand Down Expand Up @@ -818,7 +822,7 @@ def workflow_runner(self,
stack_exec.enter_context(mock.patch(data_source_use, side_effect=self.mock_get_data_source_from_url))
if self.is_webtest():
# mock execution when running on local Web Test app since no Celery runner is available
for mock_exec in mocked_execute_celery():
for mock_exec in mocked_execute_celery(web_test_app=self.app):
stack_exec.enter_context(mock_exec)
# mock HTTP HEAD request to validate WPS output access (see 'setUpClass' details)
mock_req = stack_exec.enter_context(mocked_wps_output(self.settings, mock_head=True, mock_get=False))
Expand Down
Loading

0 comments on commit 9b76b22

Please sign in to comment.