Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2 beta: Django connector seems broken #934

Closed
ewjoachim opened this issue Feb 11, 2024 · 59 comments
Closed

v2 beta: Django connector seems broken #934

ewjoachim opened this issue Feb 11, 2024 · 59 comments

Comments

@ewjoachim
Copy link
Member

ewjoachim commented Feb 11, 2024

This is about the v2 beta testing.

From discord, from @paulzakin:

error connecting in 'pool-1': connection failed: could not receive data from server: Connection refused
error connecting in 'pool-1': connection failed: could not receive data from server: Connection refused
error connecting in 'pool-1': connection failed: could not receive data from server: Connection refused
error connecting in 'pool-1': connection failed: could not receive data from server: Connection refused
Traceback (most recent call last):
  File "/Users/code/server/worker.py", line 39, in <module>
    main()
  File "/Users/code/server/worker.py", line 35, in main
    x.run_worker()  # pyright: ignore [reportUnknownMemberType]
    ^^^^^^^^^^^^^^
  File "/Users/code/.venv/lib/python3.11/site-packages/procrastinate/app.py", line 270, in run_worker
    asyncio.run(f())
  File "/Users/foo/.local/share/mise/installs/python/3.11.7/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/Users/foo/.local/share/mise/installs/python/3.11.7/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/Users/foo/.local/share/mise/installs/python/3.11.7/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/Users/code/.venv/lib/python3.11/site-packages/procrastinate/app.py", line 267, in f
    async with self.open_async():
  File "/Users/code/.venv/lib/python3.11/site-packages/procrastinate/utils.py", line 194, in __aenter__
    await self._open_coro()
  File "/Users/code/.venv/lib/python3.11/site-packages/procrastinate/psycopg_connector.py", line 170, in open_async
    await self._async_pool.open(wait=True)  # type: ignore
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/code/.venv/lib/python3.11/site-packages/psycopg_pool/pool_async.py", line 382, in open
    await self.wait(timeout=timeout)
  File "/Users/code/.venv/lib/python3.11/site-packages/psycopg_pool/pool_async.py", line 169, in wait
    raise PoolTimeout(f"pool initialization incomplete after {timeout} sec")
psycopg_pool.PoolTimeout: pool initialization incomplete after 30.0 sec

I invoke the entry with poetry run python server/worker.py
And the entry looks like this

import django

django.setup()

import procrastinate
from procrastinate.contrib.django import app

from server.env import EnvService
from server.services.db import ProcrastinateService

ENV = EnvService.get_env()
env = EnvService.get_env_variables(ENV)

connector = procrastinate.PsycopgConnector(
    kwargs={
        "host": env["POSTGRES_HOST"],
        "user": env["POSTGRES_USER"],
        "password": env["POSTGRES_PASSWORD"],
        "port": env["POSTGRES_PORT"],
        "dbname": env["POSTGRES_DB"],
        **EnvService.get_ssl(),
    }
)


def main() -> None:
    x = app.with_connector(connector)
    blueprints = ProcrastinateService.get_blueprints(show_warnings=True)
    for blueprint, namespace in blueprints:
        x.add_tasks_from(blueprint, namespace=namespace)
    x.run_worker()  # pyright: ignore [reportUnknownMemberType]


if __name__ == "__main__":
    main()

And the way I used to run it was poetry run procrastinate --app=server.worker.app worker
And I used to define app = procrastinate.App(connector=connector) in the settings.py basically and then import that into worker.py
Even more interesting: when all I do is change the version to v2 and leave all the code as it is (no django stuff changed), app defined in settings.py and then invoked in worker.py, I get that same error pool initialization incomplete after 30.0 sec.

So (my guess) when I invoke poetry run procrastinate --app=server.worker.app worker --concurrency=1 with the imported app on the new version, something changed.

@ewjoachim
Copy link
Member Author

Suggestion: could it be that you're hitting this ? What version of psycopg are you using ?

@ewjoachim
Copy link
Member Author

ewjoachim commented Feb 11, 2024

I'm trying the following script and it works as expected:

from __future__ import annotations

import django

django.setup()

from os import environ

import procrastinate
from procrastinate.contrib.django import app

connector = procrastinate.PsycopgConnector(
    kwargs={
        "host": environ["POSTGRES_HOST"],
        "user": environ["POSTGRES_USER"],
        "password": environ["POSTGRES_PASSWORD"],
        "port": environ["POSTGRES_PORT"],
        "dbname": environ["POSTGRES_DB"],
    }
)


def main() -> None:
    x = app.with_connector(connector)
    x.run_worker()  # pyright: ignore [reportUnknownMemberType]


if __name__ == "__main__":
    main()

And it works as expected with the following envvars:

export POSTGRES_HOST=$PGHOST POSTGRES_USER=$PGUSER POSTGRES_PASSWORD=$PGPASSWORD POSTGRES_PORT=5432 POSTGRES_DB=$PGDATABASE DJANGO_SETTINGS_MODULE=procrastinate_demos.demo_django.project.settings

(from the dev env, so PG variables are set to match the docker connection defined here.

@paulzakin
Copy link
Contributor

OK this is great, I can look at this on Monday! I am using 3.1.18. What are you using? This will be a good way for us to sanity check.

@ewjoachim
Copy link
Member Author

I'll triple check bit I'm using the dev env so all my versions are following the poetry lock file.

@ewjoachim
Copy link
Member Author

Yep, 3.1.18

@paulzakin
Copy link
Contributor

Ok, very odd. Now when I install 2.0.0b1 and test it out, I get this error

Traceback (most recent call last):
  File "/foo/erms/manage.py", line 23, in <module>
    main()
  File "/foo/erms/manage.py", line 19, in main
    execute_from_command_line(sys.argv)
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 413, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 459, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/core/management/base.py", line 107, in wrapper
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/core/management/commands/migrate.py", line 117, in handle
    executor = MigrationExecutor(connection, self.migration_progress_callback)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/db/migrations/executor.py", line 18, in __init__
    self.loader = MigrationLoader(self.connection)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/db/migrations/loader.py", line 58, in __init__
    self.build_graph()
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/db/migrations/loader.py", line 276, in build_graph
    self.graph.validate_consistency()
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/db/migrations/graph.py", line 198, in validate_consistency
    [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/db/migrations/graph.py", line 198, in <listcomp>
    [n.raise_error() for n in self.node_map.values() if isinstance(n, DummyNode)]
     ^^^^^^^^^^^^^^^
  File "/foo/erms/.venv/lib/python3.11/site-packages/django/db/migrations/graph.py", line 60, in raise_error
    raise NodeNotFoundError(self.error_message, self.key, origin=self.origin)
django.db.migrations.exceptions.NodeNotFoundError: Migration procrastinate.0025_initial dependencies reference nonexistent parent node ('procrastinate', '0024_job_id_bigint')

It creates a <procrastinate migrations virtual path> folder in the root with migration 0025, which does not seem right.

Is this a weird thing on my machine?

@paulzakin
Copy link
Contributor

And looking at https://github.com/procrastinate-org/procrastinate/releases/tag/2.0.0b1 - it looks like it is behind on main but a bunch of commits - is it force cutting a new beta release maybe with those commits?

@paulzakin
Copy link
Contributor

And as a sanity check, I re-cloned out repo and resinstalled everything with v1.1.2 and that worked fine.

@ewjoachim
Copy link
Member Author

I’m not on my computer to flesh out a complete answer but I’m pretty sure it’s a __pycache__ issue. We should open a different issue for that

@paulzakin
Copy link
Contributor

Ya, no rush obviously - 1.1.2 is working great currently!

@ewjoachim ewjoachim mentioned this issue Feb 17, 2024
10 tasks
@ewjoachim
Copy link
Member Author

Ok, the migration issue is solved in #941.

Now back to the connection issue. Anything you can share to help investigation is highly appreciated :)

@ewjoachim
Copy link
Member Author

And looking at 2.0.0b1 (release) - it looks like it is behind on main but a bunch of commits - is it force cutting a new beta release maybe with those commits?

Sure ! I thought you had mentionned that it was the same to you testing on main and testing on a tag. Either way work for me.

https://github.com/procrastinate-org/procrastinate/releases/tag/2.0.0b2

@ewjoachim
Copy link
Member Author

BTW, instead of:

connector = procrastinate.PsycopgConnector(
    kwargs={
        "host": environ["POSTGRES_HOST"],
        "user": environ["POSTGRES_USER"],
        "password": environ["POSTGRES_PASSWORD"],
        "port": environ["POSTGRES_PORT"],
        "dbname": environ["POSTGRES_DB"],
    }
)
x = app.with_connector(connector)

it might be worth trying:

x = app.with_connector(app.connector.get_worker_connector())

@paulzakin
Copy link
Contributor

OK, excited to debug this!

Dumb question - I don't see a release on pypi for the next version?

Also, what is the difference between pypi and "main" of procrastinate. If it is the same I can just have poetry install from a git tag, rather than pypi, but I'd like to be sure :)

@ewjoachim
Copy link
Member Author

Dumb question - I don't see a release on pypi for the next version?

Dammit, the workflow had failed. I
https://github.com/procrastinate-org/procrastinate/actions/runs/7942921413/attempts/1
Second attempt worked: https://github.com/procrastinate-org/procrastinate/actions/runs/7942921413/job/21737931035

Also, what is the difference between pypi and "main" of procrastinate. If it is the same I can just have poetry install from a git tag, rather than pypi, but I'd like to be sure :)

Installing from main is poetry add git+https://github.com/procrastinate-org/procrastinate.git#main (you could remove main, it's the default branch)
Installing from pypi is simply the normal install, provided I've done a release.
Both are the same code I think the only difference is that installing from main, procrastinate's version will be detected through different means.
When installing from main, pip freeze will report procrastinate @ git+https://github.com/procrastinate-org/procrastinate.git@7d15ef345ccf209b814776c201568d45f7979be9, and print(procrastinate.__version__) as well as importlib.metadata.metadata("procrastinate")["version"] will report 2.0.0b2.post4.dev0+7d15ef3,

@paulzakin
Copy link
Contributor

Awesome, I'll check this out tomorrow now

@paulzakin
Copy link
Contributor


Traceback (most recent call last):
  File "/Users/foo/manage.py", line 23, in <module>
    main()
  File "/Users/foo/manage.py", line 19, in main
    execute_from_command_line(sys.argv)
  File "/Users/foo/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/Users/foo/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 416, in execute
    django.setup()
  File "/Users/foo/.venv/lib/python3.11/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/Users/foo/.venv/lib/python3.11/site-packages/django/apps/registry.py", line 91, in populate
    app_config = AppConfig.create(entry)
                 ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/foo/.venv/lib/python3.11/site-packages/django/apps/config.py", line 123, in create
    mod = import_module(mod_path)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/me/.local/share/mise/installs/python/3.11.7/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/django/apps.py", line 12, in <module>
    from . import django_connector
  File "/Users/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/django/django_connector.py", line 13, in <module>
    from procrastinate.contrib.aiopg import aiopg_connector
  File "/Users/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/aiopg/__init__.py", line 3, in <module>
    from .aiopg_connector import AiopgConnector
  File "/Users/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/aiopg/aiopg_connector.py", line 8, in <module>
    import aiopg
ModuleNotFoundError: No module named 'aiopg'

@paulzakin
Copy link
Contributor

Are you expected to install aiopg to get this to work? I thought we were switching to psycopg3 because it has a good async interface? I'm happy to install aiopg, just wanted to check.

@hyusetiawan
Copy link

want to point out 2 things:

  1. from procrastinate.contrib.django import app throws an error, no variable named "app" is exported, there is "apps" though
  2. from procrastinate.contrib.aiopg import AiopgConnector will throw a confusing error that aiopg module is not found, which made me think that it's procrastinate aiopg but in reality installing aiopg separately made it work

@ewjoachim
Copy link
Member Author

@paulzakin

Are you expected to install aiopg to get this to work?

Ah indeed, I could have made it clearer in the docs, and you're right that it might not be the best choice:

It's all in https://github.com/procrastinate-org/procrastinate/blob/main/procrastinate/contrib/django/django_connector.py#L223

I've added the psycopg3 compat but I haven't removed Aiopg, however flawed may it be. When determining which one to use, I look at the is_psycopg3 bool from Django that tells me whether Django is configured to use psycopg2 or 3. If psycopg2, I use the psycopg2-based Aiopg, if psycopg3, I use psycopg3. If your procrastinate is aiming for aiopg, it means you're using a pre-psycopg3 django version.

But then, given that people might have psycopg2 installed but not aiopg, and that I'm going to make a different connection altogether, maybe I should rather:

  • try to see which one of psycopg3 and aiopg is installed and use it
  • provide a parameter to choose explicitly

I'll fix this and make it clearer in the docs.

@ewjoachim
Copy link
Member Author

@hyusetiawan your comment makes me think that you're probably using the last stable release v1.1.2, while this issue is (sorry, unclearly) about the v2 beta.

As far as I can tell, the docs are configured to still point to 1.1.2, so maybe you're looking at the docs on the main branch ?

That being said, you're more than welcome to try and integrate v2.0.0b2 (it's worth re-reading the Django docs completely) and report any problem here. The v2 had the django integration completely re-done, and in theory it should be much smoother. That said, @paulzakin can attest that so far we're still experiencing road bumps.

@ewjoachim ewjoachim changed the title Django connector seems broken v2 beta: Django connector seems broken Feb 21, 2024
@hyusetiawan
Copy link

ah okay, apologies for the confusion! it's stated in the title even that it's for v2 beta :)

@ewjoachim
Copy link
Member Author

Well... It was not stated on the title at the time of your comment :)

image

@paulzakin
Copy link
Contributor

Ok great @ewjoachim - let me know when you want to me to retest :)

@ewjoachim
Copy link
Member Author

ewjoachim commented Feb 25, 2024

v2.0.0b3 is out, you can re-test.

@paulzakin
Copy link
Contributor

paulzakin commented Feb 26, 2024

Just tested - no dice :(

If you point me in the right direction in the library I can maybe see what logic is not being tripped or not?

Traceback (most recent call last):
  File "/foo/manage.py", line 23, in <module>
    main()
  File "/foo/manage.py", line 19, in main
    execute_from_command_line(sys.argv)
  File "/foo/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/foo/.venv/lib/python3.11/site-packages/django/core/management/__init__.py", line 416, in execute
    django.setup()
  File "/foo/.venv/lib/python3.11/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/foo/.venv/lib/python3.11/site-packages/django/apps/registry.py", line 91, in populate
    app_config = AppConfig.create(entry)
                 ^^^^^^^^^^^^^^^^^^^^^^^
  File "/foo/.venv/lib/python3.11/site-packages/django/apps/config.py", line 123, in create
    mod = import_module(mod_path)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/foo/.local/share/mise/installs/python/3.11.7/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/django/apps.py", line 12, in <module>
    from . import django_connector
  File "/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/django/django_connector.py", line 13, in <module>
    from procrastinate.contrib.aiopg import aiopg_connector
  File "/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/aiopg/__init__.py", line 3, in <module>
    from .aiopg_connector import AiopgConnector
  File "/foo/.venv/lib/python3.11/site-packages/procrastinate/contrib/aiopg/aiopg_connector.py", line 8, in <module>
    import aiopg
ModuleNotFoundError: No module named 'aiopg'

@ewjoachim
Copy link
Member Author

ewjoachim commented Mar 2, 2024

It looks like the task exists in the process where you defer, but not in the process where you run the worker.

I'm trying to come up with ideas of how that may happen. Can you confirm that:

  • On both processes you start from the procrastinate.contrib.django.app instance (and then on the worker you use app.with_connector(app.connector.get_worker_connector()) which ensure you have the same tasks)
  • In both processes, Django is started (either ./manage.py or django.setup() so that we know the AppConfig.ready() has run
  • Both processes have the same settings (especially PROCRASTINATE_ON_APP_READY)

One thing you can test is go to the AppConfig (in apps.py) of any of your apps that is after procrastinate in INSTALLED_APPS and add:

def ready(self):
    from procrastinate.contrib.django import app
    print(app.tasks)

On my side, I'm adding some logs, but from your pasted output, it looks like you're not using debug logs, and not formatting them to get the most of them. (structured element).

In this PR, I'm providing concrete examples of how you can get all the structured log values without having to add a structured logging library.

Output of those logs look like:

2024-03-02 10:33:13,008 INFO    procrastinate.blueprints Adding tasks from blueprint {'action': 'loading_blueprint', 'namespace': 'bp', 'tasks': ['bp:procrastinate_demos.demo_django.demo.tasks.set_indexed']}
2024-03-02 10:33:13,039 INFO    procrastinate.blueprints Adding tasks from blueprint {'action': 'loading_blueprint', 'namespace': 'builtin', 'tasks': ['builtin:builtin:procrastinate.builtin_tasks.remove_old_jobs']}
Launching a worker on all queues
2024-03-02 10:33:13,086 DEBUG   procrastinate.app All tasks imported {'action': 'imported_tasks', 'tasks': ['builtin:procrastinate.builtin_tasks.remove_old_jobs', 'procrastinate.builtin_tasks.remove_old_jobs', 'procrastinate_demos.demo_django.demo.tasks.index_book', 'bp:procrastinate_demos.demo_django.demo.tasks.set_indexed']}
2024-03-02 10:33:13,086 INFO    procrastinate.worker Starting worker on all queues {'action': 'start_worker', 'worker': {'name': 'worker', 'id': None, 'queues': None}, 'queues': None}

Which is more informative :)

@paulzakin
Copy link
Contributor

paulzakin commented Mar 2, 2024

Ok! Experiment #1.

I defined a custom app in settings.py and then imported it throughout the Django app - it all works beautifully. Our application may be "weird enough" that using our app as good test case for the Django integration may be a bad idea...

# settings.py
connector = procrastinate.PsycopgConnector(
    kwargs={
        "host": env["POSTGRES_HOST"],
        "user": env["POSTGRES_USER"],
        "password": env["POSTGRES_PASSWORD"],
        "port": env["POSTGRES_PORT"],
        "dbname": env["POSTGRES_DB"],
        **EnvService.get_ssl(),
    }
)

command = " ".join(sys.argv)
is_django = "runserver" in command or "gunicorn" in command
app = procrastinate.App(connector=connector)
if is_django:
    app.open()

@paulzakin
Copy link
Contributor

paulzakin commented Mar 2, 2024

However, I thought I'd do some more digging to see if I can help you out for other Django people / to satisfy my own curiosity.

(PS: I added structured logging, did not really seem to help much)

The code below is the crux of the issue. As an experiment, I went "all in" on the Django integration, including running it though the worker. And everything worked great - except for this. Basically, PROCRASTINATE_ON_APP_READY works. The tasks were registered in both the Django and Procrastinate process. So example #2 works great.

However, example #2 requires a helper function that imports the app at the time of invocation. If you do not do that, and just run it off the imported app, you get that If this message appears at import time, the app is not ready yet. If your answer is to just do the code like example #2 or put from procrastinate.contrib.django import app INSIDE the function (also works), that is totally fine with me!

However, I just want to understand if this behaviour makes sense to you / am I doing things wrong (lol)

# Helper function
import procrastinate
class WorkerService:
    @classmethod
    def app(cls) -> procrastinate.App:
        from procrastinate.contrib.django import app
        return app
from procrastinate.contrib.django import app
# This function is executed in a Django request
def schedule_report_to_be_run() -> None:
    name = "Foo"
    app.configure_task(name=name, queueing_lock=name).defer(name="Bar") # Example 1. Does not work
    WorkerService.app().configure_task(name=name, queueing_lock=name).defer(name="Bar") # Example 2. Works

@ewjoachim
Copy link
Member Author

Aaaaah yes of course.

It will also work if you keep the import outside but do from procrastinate.contrib import django and django.app.

The problem is that when we do contrib_django.app = create_app(blueprint=contrib_django.app), whoever has already imported the app and kept a reference will continue to use the old instance.

Let's see if I can do something about that. Might have to use a proxy instance.

@ewjoachim
Copy link
Member Author

I've added a proxy object, so now, importing the object before it's ready and keeping a reference to it, it will work after the real app is swapped in place.

https://github.com/procrastinate-org/procrastinate/releases/tag/2.0.0b7
https://pypi.org/project/procrastinate/2.0.0b7/

@ewjoachim
Copy link
Member Author

Our application may be "weird enough" that using our app as good test case for the Django integration may be a bad idea...

If you had to do weird stuff, everyone had to do weird stuff :)

@paulzakin
Copy link
Contributor

Ok excited to test this!

Also is there a way to add a flag to disable this? For example, we use our Procrastinate/ Django models to insert stuff during tests - I guess switching to the Procrastinate managed connection or models prevents this? Also I feel like I asked about the third Procrastinate / Django model - did that ever get exposed?

Procrastinate models exposed in Django, such as ProcrastinateJob are read-only. Please use the procrastinate CLI to interact with jobs.

@ewjoachim
Copy link
Member Author

ewjoachim commented Mar 3, 2024

I guess I can add a setting to control that, and maybe a pytest fixture too.

That said, in many cases, I'd say it should be easier to run tests with the InMemoryConnector that is specifically made for testing, exposed and documented.

I'd say it depends if we're talking about unit tests or integration test.

Btw, I could even expose FactoryBoy factories.

So:

  • PROCRASTINATE_READONLY_MODELS that can be updated on specific tests (showing the code on how to make a fixture should be nice)
  • maybe a fixture that would replace the contrib app connector with an InMemoryConnector (giving the code may be more usable than providing the object)
  • maybe a set of factories for the models (same, and would avoid a dependency. Downside: not testable)
  • yes ProcrastinatePeriodicDefer is exposed, and in the PR I forgot to change the doc to mention it. Maybe add a small reference doc too, would be nice.
  • the django documentation should be a whole section, not just a page.

@paulzakin
Copy link
Contributor

They are integration tests - so having access to the whole thing would be nice! I can make due without them - but it would be helpful

@paulzakin
Copy link
Contributor

paulzakin commented Mar 3, 2024

Good news - the proxy works!

Other news:

Procrastinate models exposed in Django, such as ProcrastinateJob are read-only. Please use the procrastinate CLI to interact with jobs. Only seems to apply to procrastinate_jobs and procrastinate_events. It does not trigger for procrastinate_periodic_defers. Is that by design?

@ewjoachim
Copy link
Member Author

Is that by design?

Nope, will fix that too.

@ewjoachim
Copy link
Member Author

(I'm removing the DB router from the next beta release. The more I use it, the less I'm convinced it's useful and it will make it harder for some of the changes. If you did set it up, you can remove it.)
(PR is ready but I'll need a bit more time before merging and releasing)

@paulzakin
Copy link
Contributor

Sounds good!

@ewjoachim
Copy link
Member Author

Beta numbers are cheap

https://github.com/procrastinate-org/procrastinate/releases/tag/2.0.0b8
https://pypi.org/project/procrastinate/2.0.0b8/

The django doc was split into individual pages: https://procrastinate.readthedocs.io/en/main/howto_index.html

There are now ways to relax the readonly feature for tests, plus, it's explained how to get a testing connector for the Django app

@paulzakin
Copy link
Contributor

Ok, more progress and those docs look great.

Two things:

Otherwise PROCRASTINATE_READONLY_MODELS solved the problem with tests, so I think with just that healthcheck fix we are good to go!

@ewjoachim
Copy link
Member Author

ewjoachim commented Mar 7, 2024

Is there a way to run healthchecks via the django command?

Hm, healthchecks look at 3 things:

  • App config (actually it's a noop, it's just that if the app wasn't configured correctly, we would have crashed before reaching this point)
  • DB connection
  • The presence of the procrastinate_jobs table

Within Django:

  • The app config is mostly the responsibility of Procrastinate itself
  • Checking the DB configuration is the responsibility of Django. I feel that if Django's DB wasn't configured properly, it wouldn't be Procrastinate's decision to raise this.
  • The presence of the procrastinate_jobs table is the responsibility of Django migrations. This is checked by ./manage.py check. (Technically, it's even better with Django because it checks that all migrations were applied whereas Procrastinate only checks the 1st migration)

Because of all that, I didn't include the healthchecks command to what was exposed. I could re-add it back but I'm not sure that it would add benefits vs a false sense of security ?

I could, though, be easily convinced to add it back if you have ideas of things that would make sense checking ? Is your usecase more of a sanity-check for your integration, or a runtime check used as liveness/readiness check as a basis for killing dead pods ? Also, should that be a healthcheck validating whether we're able to defer tasks, or to run the worker ?

For now I have more questions than answers :D


I've fixed PROCASTINATE_READONLY_MODELS once, but I guess I had copy-pasted the error elsewhere. Will fix it.

@ewjoachim ewjoachim mentioned this issue Mar 7, 2024
10 tasks
@paulzakin
Copy link
Contributor

All reasonable points!

So if you imagine a Procrastinate & Django application living in a container or pod, and the command is python manage.py procrastinate worker, in your mind what is the best way to judge if the worker is running correctly? So, yup, in your words, a "runtime check used as liveness/readiness check as a basis for killing dead pods".

I only ever used it to check whether I could run the worker at all, defering tasks would not be needed for us. Would be good though!

I agree though, overall, that the Django connection does obviate some of the healthcheck stuff, as a lot of it is now handled by Django!

@paulzakin
Copy link
Contributor

Also, side point, is there any way to get on_app_ready to only run once - it seems to run multiple times - once during make migrations, then during migrate, then during app load. That may just be a Django quirk, in which case ignore this :)

import procrastinate
def on_app_ready(app: procrastinate.App) -> None:
    print("FOO")
    # Do stuff with app

@ewjoachim
Copy link
Member Author

It seems to run multiple times - once during make migrations, then during migrate, then during app load

From my understanding of your setup, the issue is that the 3 things you mention are 3 separate processes. The things you do in on_app_ready are in-memory changes (such as registering a blueprint, this is not persisted anywhere) so each process will do its own django.setup(). This is less a Django quirk and more a consequence of your setup?

There may be multiple things you can do:

  • Adjust the logging level of Procrastinate so that it won't be so noisy when you don't need it to be. (I'm guessing the reason you don't want to run it multiple times is that it clutters your logs?)
  • Potentially adjust you settings based on sys.stdin so that your setup is different when you run runserver vs other commands. Alternately, use environment variables to explicitly trigger different setup steps ?
  • Launch everything is a single process: a single script that launches multiple call_command() calls?

As a side note, it's perfectly reasonable that your container launches ./manage.py migrate on startup (though it might get complicated if you have multiple containers launching at once: if they try to migrate the same DB in parallel you'll have issues. But makemigrations is definitely not something you should run automatically as part of your runtime. It should be made during development, and migrations should be committed alongside with the modifications of the model they relate to.

@paulzakin
Copy link
Contributor

Yup, that answers my 2nd comment. And yup, the command makemigrations is just run on development!

As for the first comment, the healthcheck, does that answer your question? Otherwise, I think the integration is done, at least from my perspective?

@ewjoachim
Copy link
Member Author

As for the first comment, the healthcheck, does that answer your question? Otherwise, I think the integration is done, at least from my perspective?

Ah yes, still pondering on what to do about that (if anything). I think I understand your usecase and it makes sense, but I think it might be better answered than just by activating the healthcheks as they exist today, on Django.

I see 2 choices, and we can have both:

  • A <something> that tells you if the worker process is doing right. I'd tend to say that the most appropriate <something> that comes to mind would be an HTTP endpoint with /alive, /ready and potentially /metrics (from Prometheus)
  • A process that tells you if another process could theoretically work. It's kinda what the healthcheck command is. I think it's more about checking that the app is configured correctly and the code is right rather than checking the state of the setup right now, though it might be a side effect. I'm thinking that integrating the Django checks framework and doing our own checks might be the best way to do that, so that ./manage.py checks (maybe with --tags procrastinate) would do the trick. I'll look at that.

@paulzakin
Copy link
Contributor

paulzakin commented Mar 8, 2024

Ya, the second option seems best.

This is a really cool integration BTW, I think it will really help with people using the library :)

@ewjoachim
Copy link
Member Author

So, I tried integrating with Django checks framework and... It just didn't match what I wanted to do. Ended up just doing a custom ./manage.py procrastinate healthchecks with some specific Django tests. Probably isn't 100% useful, but it shakes the whole tree and see if anything falls.

https://github.com/procrastinate-org/procrastinate/releases/tag/2.0.0b9
https://pypi.org/project/procrastinate/2.0.0b9/
https://procrastinate.readthedocs.io/en/main/howto/django/basic_usage.html#checking-proper-configuration

Hopefully, we're good to go :shipit:

@paulzakin
Copy link
Contributor

It works beautifully! Closed, as far as I am concerned :)

@ewjoachim
Copy link
Member Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants