• 
      

    Add initial support for message broker backends.

    Review Request #15196 — Created July 24, 2026 and updated

    Information

    Review Board
    release-9.x

    Reviewers

    This introduces reviewboard.broker, which provides official support in
    Review Board for communicating with message brokers (such as RabbitMQ)
    and sending tasks or broadcasts to any workers connected to the broker.

    This initial change offers base broker backend support, a local
    filesystem-based broker, a broker registry, and the beginnings of broker
    configuration.

    A broker backend is responsible for connecting to a broker service,
    allowing tasks to be sent or messages to be broadcast to all active
    workers, and gathering worker status.

    We're using Celery for the main broker work, since that's pretty
    complete, but the architecture doesn't mandate this. BaseBrokerBackend
    doesn't care about the transport, while BaseCeleryBrokerBackend
    manages all the Celery state (and ties it to the instance, rather than
    registering a global Celery instance).

    Subclasses using Celery simply need to inherit from
    BaseCeleryBrokerBackend and override get_celery_config() to return
    the configuration and broker URI needed.

    There's currently a single built-in filesystem-based broker. It stores
    messages and worker registrations in the site's data directory for local
    workers to access. This is a default that will be usable with a future
    version of Review Bot and with an upcoming local-only companion worker
    responsible for background tasks.

    Future changes will implement worker scanning, more backends,
    configuration, and the local background task companion worker.

    Unit tests pass.

    Tested the basic functionality in combination with other changes and
    a modified Review Bot.

    Summary ID
    Add initial support for message broker backends.
    This introduces `reviewboard.broker`, which provides official support in Review Board for communicating with message brokers (such as RabbitMQ) and sending tasks or broadcasts to any workers connected to the broker. This initial change offers base broker backend support, a local filesystem-based broker, a broker registry, and the beginnings of broker configuration. A broker backend is responsible for connecting to a broker service, allowing tasks to be sent or messages to be broadcast to all active workers, and gathering worker status. We're using Celery for the main broker work, since that's pretty complete, but the architecture doesn't mandate this. `BaseBrokerBackend` doesn't care about the transport, while `BaseCeleryBrokerBackend` manages all the Celery state (and ties it to the instance, rather than registering a global `Celery` instance). Subclasses using Celery simply need to inherit from `BaseCeleryBrokerBackend` and override `get_celery_config()` to return the configuration and broker URI needed. There's currently a single built-in filesystem-based broker. It stores messages and worker registrations in the site's data directory for local workers to access. This is a default that will be usable with a future version of Review Bot and with an upcoming local-only companion worker responsible for background tasks. Future changes will implement worker scanning, more backends, configuration, and the local background task companion worker.
    8d31cffb8db21f1e9c5b0d65a97b90262b027a11
    Description From Last Updated

    There's no reviewboard/broker/base/__init__.py file (exacerbated by us using namespaces = false in pyproject.toml's tool.setuptools.packages.find)

    david david

    New files need to be added to coderef/index.rst

    david david

    One thought I just had: the registry holds backend classes, but the backends themselves hold per-instance state (like locks and …

    david david

    BaseBrokerBackend creates a self.logger but everything in here uses a module-level logger.

    david david

    This logs the message but never resets the value. We'd then hit the assertion below in dev, or just crash …

    david david

    We should probably create with a more locked-down permissions mask.

    david david

    I don't know if you've had a chance to look at the spec I put together on tasks, but I …

    david david

    This doesn't match the implementation/tests. For send_task, the command name is in the headers (message_headers['task']) and the body is [args, …

    david david

    This should probably protect with threading.Lock. It would be nice to also listen to the siteconfig sync (or put in …

    david david

    There's a lot of duplication in here. It might be nice to have a _get_broker_settings(broker_id) helper which can validate and …

    david david

    This is using a module-level logger instead of self.logger. Same for the other one just below.

    david david

    Celery's ctor doesn't define a name arg, but it does take in **kwargs and silently ignore anything it doesn't know …

    david david

    These should match the same order as they appear in the signature.

    david david

    This is passing instance as a positional arg, but the first argument for KeyValueForm.__init__() is data. We need to explicitly …

    david david

    Can we format as: from djblets.registries.registry import ( ALREADY_REGISTERED, ... UNREGISTER, )

    david david

    This is defining the entry point but the class doesn't inherit from EntryPointRegistry. If we fix that we also need …

    david david

    Our other registries use %(item)s. As-is this will render as: "<class 'LocalBrokerBackend'>" is already a registered broker backend. (with both …

    david david

    Same comment about %(item)s vs %(item)r

    david david

    Should be reviewboard.broker.base.backends.BaseBrokerBackend

    david david

    undefined name 'JSONDict' Column: 33 Error code: F821

    reviewbot reviewbot

    It looks like this is using an outdated name for get_celery_config. Same for other tests below.

    david david

    Any time we open a file in text mode we should pass encoding=. When that's present I prefer to also …

    david david

    The mode here only applies to the leaf directory. If we want all dirs to be set to that we …

    david david

    'typing.cast' imported but unused Column: 1 Error code: F401

    reviewbot reviewbot

    Something got weird here: "If state had to be added to repaired" Was that supposed to be "added or repaired"?

    david david

    We should wrap this with with self._lock:.

    david david

    get_broker_settings() already is typed as returning a dict. If that dict is empty, we then return a separate dict instead. …

    david david

    message_files is already joined to queue_dir because of the. glob() call. We should be able to just do with open(message_file, …

    david david

    This could fit on one line.

    david david

    Should sort before cryptography

    david david

    Maybe clarify that this is only on Linux systems?

    david david

    This can go in the if TYPE_CHECKING block

    david david

    This is pointing to some very old celery docs. Can we point to /en/stable/internals/protocol.html?

    david david

    Do we want to raise an exception if this is empty? Base class sets to '', so we're currently silently …

    david david

    This should include a "Raises" section with kombu.exceptions.OperationalError

    david david

    This should include a "Raises" section with kombu.exceptions.OperationalError

    david david

    We discussed it on the above issue but it never got fixed: can we get rid of or {} here?

    david david

    This is operating somewhat differently to other stuff in the codebase which is similar: This is fetching its own siteconfig …

    david david

    This is unused.

    david david

    Typo: extra space between BaseBroker and Backend

    david david

    Does this work if you run the test in isolation, or is it silently order-dependent on the other tests that …

    david david

    Do we want to pin this narrow, or do ">=5.6.3,<6"?

    david david
    Checks run (1 failed, 1 succeeded)
    flake8 failed.
    JSHint passed.

    flake8

    david
    1. 
        
    2. Show all issues

      There's no reviewboard/broker/base/__init__.py file (exacerbated by us using namespaces = false in pyproject.toml's tool.setuptools.packages.find)

    3. reviewboard/broker/backends/local.py (Diff revision 1)
       
       
      Show all issues

      BaseBrokerBackend creates a self.logger but everything in here uses a module-level logger.

    4. reviewboard/broker/backends/local.py (Diff revision 1)
       
       
       
       
       
      Show all issues

      This logs the message but never resets the value. We'd then hit the assertion below in dev, or just crash in other ways in prod.

    5. reviewboard/broker/backends/local.py (Diff revision 1)
       
       
       
      Show all issues

      We should probably create with a more locked-down permissions mask.

      1. Hmm yeah. Let me get your thoughts on the options here. Might point to a larger design question.

        By default, the Review Board web server process will spawn this, so it'll be that user (say, www-data). 700 would be a good choice here then.

        If we want to allow systemd management of the process, it'd need to either be the same user or a user that can also write to this path. So then, 770 I guess. But then we'd also have to somehow have ownership changes here, which that user can't do.

        I'm realizing as I write this that maybe putting any of this in the data directory is the wrong approach to begin with. The NFS issue is one thing, since a shared queue directory across processes with a shared site data directory could lead to some issues. Should we keep it all local-only, write to some place like in /var/spool (configurable), and then pass that path to the worker process? Then more advanced setups could deal with the directory creation and management as they need to.

      2. I feel like the local broker should really only be a fallback for the simplest deployments, and that for that case, the broker and worker should be managed entirely by the web server process.

        If someone wants systemd, they should also be setting up a real broker.

    6. reviewboard/broker/backends/local.py (Diff revision 1)
       
       
      Show all issues

      I don't know if you've had a chance to look at the spec I put together on tasks, but I don't think we should use a result backend.

      Instead, what I had planned out was a task model that could store (smaller) results directly, and larger things (like doc conversion or review bot) end up mapping to domain objects like reviews or file attachments.

      If we end up actually using a result backend, this would need to be fixed to use "file://localhost{queue_path}" (no extra slash). Celery strips off 16 characters for "file://localhost". For absolute queue paths that currently ends up with a double leading slash, and if somehow the queue path is a relative path, it would end up adding an extra leading slash turning it into an (incorrect) absolute path.

      1. Agreed, that'd make things a lot simpler. I was thinking we'd still need something for Review Bot's tools update, but I see now that those are registered via HTTP POSTs by the bot, so that keeps things simpler.

    7. reviewboard/broker/base/backends.py (Diff revision 1)
       
       
       
       
       
       
       
       
       
       
      Show all issues

      This doesn't match the implementation/tests.

      For send_task, the command name is in the headers (message_headers['task']) and the body is [args, kwargs, embed] as per Celery protocol 2.

      For broadcast, the body is a {'method': ..., 'arguments': ...} dict.

      1. Ah yep. When I was tracing this, I was going through the JSON serialization code, and it was assembling messages as documented above. But it was a lie. It was serializing to the above payload, then deserializing to create normalized arguments. That was before I got to the unit test code. Never corrected this.

    8. reviewboard/broker/base/backends.py (Diff revision 1)
       
       
      Show all issues

      This should probably protect with threading.Lock.

      It would be nice to also listen to the siteconfig sync (or put in a TODO comment about that).

      1. The invalidation will happen in a future change. In upcoming work, there's a central function that will return a cached broker, and siteconfig changes will invalidate all this state.

        I'll add a state lock.

    9. reviewboard/broker/base/backends.py (Diff revision 1)
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
      Show all issues

      There's a lot of duplication in here.

      It might be nice to have a _get_broker_settings(broker_id) helper which can validate and return the settings. That would be independently testable, and reusable by BaseBrokerSettingsForm.__init__, which currently does the same lookup but with no validation.

      1. Good idea. Some of the duplication is just checking for the root vs. sub-key for the setting and displaying a different error message, so I think I'll still want that, but moving this out into a utility method's the way to go.

        Somewhere along the ways, I dropped a siteconfig change that made validation like this easy..

    10. reviewboard/broker/base/backends.py (Diff revision 1)
       
       
      Show all issues

      This is using a module-level logger instead of self.logger. Same for the other one just below.

    11. reviewboard/broker/base/backends.py (Diff revision 1)
       
       
      Show all issues

      Celery's ctor doesn't define a name arg, but it does take in **kwargs and silently ignore anything it doesn't know about.

      I think this was intended to be main='reviewboard'

    12. reviewboard/broker/base/forms.py (Diff revision 1)
       
       
       
       
       
       
       
       
       
      Show all issues

      These should match the same order as they appear in the signature.

    13. reviewboard/broker/base/forms.py (Diff revision 1)
       
       
      Show all issues

      This is passing instance as a positional arg, but the first argument for KeyValueForm.__init__() is data. We need to explicitly pass instance=instance here.

    14. reviewboard/broker/registry.py (Diff revision 1)
       
       
       
       
       
      Show all issues

      Can we format as:

      from djblets.registries.registry import (
          ALREADY_REGISTERED,
          ...
          UNREGISTER,
      )
      
    15. reviewboard/broker/registry.py (Diff revision 1)
       
       
      Show all issues

      This is defining the entry point but the class doesn't inherit from EntryPointRegistry.

      If we fix that we also need a yield from super().get_defaults() inside the get_defaults() implementation.

      1. Copy/pasted from the account backends. I don't think we want entrypoints anyway. Removing.

    16. reviewboard/broker/registry.py (Diff revision 1)
       
       
      Show all issues

      Our other registries use %(item)s. As-is this will render as:

      "<class 'LocalBrokerBackend'>" is already a registered broker backend.

      (with both quotes and repr). Alternatively just remove the quotes.

      1. I think the repr is useful, but the quotes aren't needed. Removing those.

        This also came from the accounts registry, so ideally we'd fix that too, but out of scope for now.

    17. reviewboard/broker/registry.py (Diff revision 1)
       
       
      Show all issues

      Same comment about %(item)s vs %(item)r

    18. reviewboard/broker/registry.py (Diff revision 1)
       
       
      Show all issues

      Should be reviewboard.broker.base.backends.BaseBrokerBackend

    19. Show all issues

      It looks like this is using an outdated name for get_celery_config. Same for other tests below.

    20. Show all issues

      Any time we open a file in text mode we should pass encoding=. When that's present I prefer to also be explicit with mode='r'.

    21. 
        
    chipx86
    Review request changed
    Change Summary:
    • Added a missing __init__.py.
    • Added get_broker_settings(), which also populates/repairs as needed.
    • Added thread locking in get_celery() (which is now public).
    • Added unit tests for the broker base classes.
    • Added BrokerBackendRegistry.get_broker().
    • Removed the results backend and all result control when sending out tasks/broadcasts.
    • Removed entrypoint configuration in the registry and error message formats.
    • Updated BaseBrokerSettingsForm to use get_broker_settings().
    • Switched to using self.logger everywhere.
    • Set explicit directory modes to 0o700.
    • Fixed incorrect docs about the message format.
    • Fixed setting main on Celery.
    • Fixed errors in docstrings.
    • Fixed unit test names.
    Commits:
    Summary ID
    Add initial support for message broker backends.
    This introduces `reviewboard.broker`, which provides official support in Review Board for communicating with message brokers (such as RabbitMQ) and sending tasks or broadcasts to any workers connected to the broker. This initial change offers base broker backend support, a local filesystem-based broker, a broker registry, and the beginnings of broker configuration. A broker backend is responsible for connecting to a broker service, allowing tasks to be sent or messages to be broadcast to all active workers, and gathering worker status. We're using Celery for the main broker work, since that's pretty complete, but the architecture doesn't mandate this. `BaseBrokerBackend` doesn't care about the transport, while `BaseCeleryBrokerBackend` manages all the Celery state (and ties it to the instance, rather than registering a global `Celery` instance). Subclasses using Celery simply need to inherit from `BaseCeleryBrokerBackend` and override `get_celery_config()` to return the configuration and broker URI needed. There's currently a single built-in filesystem-based broker. It stores messages and worker registrations in the site's data directory for local workers to access. This is a default that will be usable with a future version of Review Bot and with an upcoming local-only companion worker responsible for background tasks. Future changes will implement worker scanning, more backends, configuration, and the local background task companion worker.
    8cea3ad0b7ef37b01547b9fc82054e2f82591cf3
    Add initial support for message broker backends.
    This introduces `reviewboard.broker`, which provides official support in Review Board for communicating with message brokers (such as RabbitMQ) and sending tasks or broadcasts to any workers connected to the broker. This initial change offers base broker backend support, a local filesystem-based broker, a broker registry, and the beginnings of broker configuration. A broker backend is responsible for connecting to a broker service, allowing tasks to be sent or messages to be broadcast to all active workers, and gathering worker status. We're using Celery for the main broker work, since that's pretty complete, but the architecture doesn't mandate this. `BaseBrokerBackend` doesn't care about the transport, while `BaseCeleryBrokerBackend` manages all the Celery state (and ties it to the instance, rather than registering a global `Celery` instance). Subclasses using Celery simply need to inherit from `BaseCeleryBrokerBackend` and override `get_celery_config()` to return the configuration and broker URI needed. There's currently a single built-in filesystem-based broker. It stores messages and worker registrations in the site's data directory for local workers to access. This is a default that will be usable with a future version of Review Bot and with an upcoming local-only companion worker responsible for background tasks. Future changes will implement worker scanning, more backends, configuration, and the local background task companion worker.
    ce753816d68d21844538512c6d18e6f7d3770240

    Checks run (1 failed, 1 succeeded)

    flake8 failed.
    JSHint passed.

    flake8

    david
    1. 
        
    2. reviewboard/broker/backends/local.py (Diff revision 2)
       
       
       
       
       
      Show all issues

      The mode here only applies to the leaf directory. If we want all dirs to be set to that we need to call os.umask() first (although maybe it's fine for parent dirs to have the default perms).

      1. I think it's fine for parent dirs to have the default perms. We only care about these dirs.

    3. reviewboard/broker/base/backends.py (Diff revision 2)
       
       
       
      Show all issues

      Something got weird here: "If state had to be added to repaired"

      Was that supposed to be "added or repaired"?

    4. reviewboard/broker/base/backends.py (Diff revision 2)
       
       
       
       
      Show all issues

      We should wrap this with with self._lock:.

    5. reviewboard/broker/base/backends.py (Diff revision 2)
       
       
      Show all issues

      get_broker_settings() already is typed as returning a dict. If that dict is empty, we then return a separate dict instead. Let's just return get_broker_settings() so we don't have to worry about aliasing issues.

      1. I don't really understand most of this comment. We're not returning a dictionary here at all and definitely not the broker settings.

        I think you mean the or {} is unnecessary since we're guaranteed a dict. I can fix that. Earlier in the design for this update, a None was an allowed value so that we could check if state was corrupted, but now it repairs.

      2. I think I meant "assign" rather than return. But yeah, just broker_settings = self.get_broker_settings()

    6. Show all issues

      message_files is already joined to queue_dir because of the. glob() call. We should be able to just do with open(message_file, ...): here.

    7. reviewboard/dependencies.py (Diff revision 2)
       
       
      Show all issues

      Should sort before cryptography

    8. 
        
    chipx86
    Review request changed
    Change Summary:
    • Renamed the top-level broker siteconfig dictionary to brokers.
    • Fixed some bad wording in the docs.
    • Celery shutdown is now protected by a lock.
    • Fixed sorting of the celery dependency in the list.
    • Other small code cleanups.
    Commits:
    Summary ID
    Add initial support for message broker backends.
    This introduces `reviewboard.broker`, which provides official support in Review Board for communicating with message brokers (such as RabbitMQ) and sending tasks or broadcasts to any workers connected to the broker. This initial change offers base broker backend support, a local filesystem-based broker, a broker registry, and the beginnings of broker configuration. A broker backend is responsible for connecting to a broker service, allowing tasks to be sent or messages to be broadcast to all active workers, and gathering worker status. We're using Celery for the main broker work, since that's pretty complete, but the architecture doesn't mandate this. `BaseBrokerBackend` doesn't care about the transport, while `BaseCeleryBrokerBackend` manages all the Celery state (and ties it to the instance, rather than registering a global `Celery` instance). Subclasses using Celery simply need to inherit from `BaseCeleryBrokerBackend` and override `get_celery_config()` to return the configuration and broker URI needed. There's currently a single built-in filesystem-based broker. It stores messages and worker registrations in the site's data directory for local workers to access. This is a default that will be usable with a future version of Review Bot and with an upcoming local-only companion worker responsible for background tasks. Future changes will implement worker scanning, more backends, configuration, and the local background task companion worker.
    ce753816d68d21844538512c6d18e6f7d3770240
    Add initial support for message broker backends.
    This introduces `reviewboard.broker`, which provides official support in Review Board for communicating with message brokers (such as RabbitMQ) and sending tasks or broadcasts to any workers connected to the broker. This initial change offers base broker backend support, a local filesystem-based broker, a broker registry, and the beginnings of broker configuration. A broker backend is responsible for connecting to a broker service, allowing tasks to be sent or messages to be broadcast to all active workers, and gathering worker status. We're using Celery for the main broker work, since that's pretty complete, but the architecture doesn't mandate this. `BaseBrokerBackend` doesn't care about the transport, while `BaseCeleryBrokerBackend` manages all the Celery state (and ties it to the instance, rather than registering a global `Celery` instance). Subclasses using Celery simply need to inherit from `BaseCeleryBrokerBackend` and override `get_celery_config()` to return the configuration and broker URI needed. There's currently a single built-in filesystem-based broker. It stores messages and worker registrations in the site's data directory for local workers to access. This is a default that will be usable with a future version of Review Bot and with an upcoming local-only companion worker responsible for background tasks. Future changes will implement worker scanning, more backends, configuration, and the local background task companion worker.
    8d31cffb8db21f1e9c5b0d65a97b90262b027a11

    Checks run (2 succeeded)

    flake8 passed.
    JSHint passed.
    david
    1. 
        
    2. reviewboard/broker/tests/test_local_backend.py (Diff revisions 2 - 3)
       
       
       
       
      Show all issues

      This could fit on one line.

      1. We've historically kept to one keyword argument per line when dealing with multiple keyword arguments, like a dictionary. I want to keep that consistency instead of changing it.

    3. 
        
    david
    1. 
        
    2. Show all issues

      New files need to be added to coderef/index.rst

    3. Show all issues

      One thought I just had: the registry holds backend classes, but the backends themselves hold per-instance state (like locks and the cached Celery instance). shutdown() only means anything if it's called on the same instance that get_celery() populated.

      Should we have the registry instead store instances? Or at least create a singleton accessor?

    4. reviewboard/broker/backends/local.py (Diff revision 3)
       
       
       
      Show all issues

      Maybe clarify that this is only on Linux systems?

    5. reviewboard/broker/base/backends.py (Diff revision 3)
       
       
      Show all issues

      This can go in the if TYPE_CHECKING block

    6. reviewboard/broker/base/backends.py (Diff revision 3)
       
       
      Show all issues

      This is pointing to some very old celery docs. Can we point to /en/stable/internals/protocol.html?

    7. reviewboard/broker/base/backends.py (Diff revision 3)
       
       
      Show all issues

      Do we want to raise an exception if this is empty? Base class sets to '', so we're currently silently hoping that subclasses override it.

    8. reviewboard/broker/base/backends.py (Diff revision 3)
       
       
      Show all issues

      This should include a "Raises" section with kombu.exceptions.OperationalError

    9. reviewboard/broker/base/backends.py (Diff revision 3)
       
       
      Show all issues

      This should include a "Raises" section with kombu.exceptions.OperationalError

    10. reviewboard/broker/base/backends.py (Diff revision 3)
       
       
      Show all issues

      We discussed it on the above issue but it never got fixed: can we get rid of or {} here?

    11. reviewboard/broker/base/forms.py (Diff revision 3)
       
       
      Show all issues

      This is operating somewhat differently to other stuff in the codebase which is similar:

      • This is fetching its own siteconfig object. A future "Message broker" page that's using these as subforms will end up having two separate siteconfig instances that get saved independently.
      • Additionally, this is the one of our KeyValueForms where the instance and the saved object are different. It's only working because the instance is identity aliased inside of siteconfig.settings.
      • It's inheriting KeyValueForm.get_key_value, so broker settings never see the siteconfig defaults layer, only field.initial. That's fine for now because the default is empty, but might bite us in the future.
      • save() is called without update_fields=('settings',), and no load_site_config() call after.

      The closest analogs to how this form is operating are SearchBackendForm and the auth/SSO backends. Those solve these issues in somewhat different ways: for earch, the SearchSettingsForm handles all the siteconfig persistence. For auth/SSO, the subforms are their own SiteSettingsForm, which works because all the auth keys are flat instead of nested objects.

      I think ideally we'd mirror what SearchBackendForm does, but at a minimum, instead of fetching our own siteconfig, we should inject it, and comment about how instance is reaching into siteconfig:

      ```python
      def init(
      self,
      siteconfig: SiteConfiguration,
      args,
      broker_cls: type[BaseBrokerBackend],
      *kwargs,
      ) -> None:
      self.broker_cls = broker_cls
      self.siteconfig = siteconfig

      super().__init__(
          *args,
          instance=broker_cls.get_broker_settings(),
          **kwargs)
      

      def save_instance(self) -> None:
      # self.instance is the dict already stored within the siteconfig's
      # broker settings, so any mutations have already been applied. We just
      # need to persist them.
      self.siteconfig.save(update_fields=('settings',))

    12. reviewboard/broker/registry.py (Diff revision 3)
       
       
      Show all issues

      This is unused.

    13. Show all issues

      Typo: extra space between BaseBroker and Backend

    14. Show all issues

      Does this work if you run the test in isolation, or is it silently order-dependent on the other tests that are setting this key?

      This method also never restores the key. How about:

      old_settings = siteconfig.settings.pop('brokers', None)
      
      if old_settings is not None:
          self.addCleanup(siteconfig.settings.__setitem__, 'brokers', old_settings)
      else:
        self.addCleanup(siteconfig.settings.pop, 'brokers', None)
      
    15. reviewboard/dependencies.py (Diff revision 3)
       
       
      Show all issues

      Do we want to pin this narrow, or do ">=5.6.3,<6"?

    16.