Repository: inventree/InvenTree

Issue:
Endless background jobs
### Please verify that this bug has NOT been raised before.

- [X] I checked and didn't find a similar issue

### Describe the bug*

I don't know how to describe it, I'll try my best, please ask if you'r missing something.

### Background
I'm onboarding a small business to InvenTree. It has a 10+ years history in eCommerce (but very small) and moved over to handmade products, so InvenTree will come into play to deal with stock. I decided to import "the past" (re-create stock movements so to say) for some resons. It's partially automated, so it's a fun deep dive into InvenTree.

### Setup
I host it on DS918+ (INTEL Celeron J3455, 1.5 GHz) with 8 GiB RAM. It runs on Docker via Protainer.

### Dimension
Currently I have 10 Parts, 10 Purchase Orders, 137 Sale Orders.  
I would say that's not much.

### Problem
I can create new customers, I can create Sale Orders. I can issue Sale Orders. All this runns quiet smooth. BUT: When I ship the order items, things start to go south.

Per shipment, roughly 200 or 300 background jobs are spawned (while being processed), which take probably 5 minutes to go back down to 0. For sure it decreases every now and then, but also it increases again.

If I ship 3 or 4 orders, the queue is averaging about 700 - 900 queued jobs, that lasts for 20 minutes or so.


### Steps to Reproduce

1. Ship a ton of orders

### Expected behaviour

Maybe it's to blue-eyed, but I would say shipping some orders will just do some database updates. That should be processed within no time.

### Deployment Method

- [X] Docker
- [ ] Bare metal

### Version Information

# Version Information:
InvenTree-Version: 0.12.7
Django Version: 3.2.19
Commit Hash: 6563b4c
Commit Date: 2023-09-16

Database: postgresql
Debug-Mode: False
Deployed using Docker: True
Platform: Linux-4.4.180+-x86_64-with-glibc2.36
Installer: DOC

Active plugins: [{'name': 'InvenTreeBarcode', 'slug': 'inventreebarcode', 'version': '2.0.0'}, {'name': 'InvenTreeCoreNotificationsPlugin', 'slug': 'inventreecorenotificationsplugin', 'version': '1.0.0'}, {'name': 'AutoFinishSalesOrderPlugin', 'slug': 'mj-auto-finish-sales-order', 'version': '0.0.0'}, {'name': 'AutoProvisionSalesOrderPlugin', 'slug': 'mj-auto-provision-sales-order', 'version': '0.0.0'}, {'name': 'OrderImportPlugin', 'slug': 'mj-order-import-plugin', 'version': '0.0.0'}, {'name': 'OrderProfitPlugin', 'slug': 'mj-order-profit-plugin', 'version': '0.0.0'}, {'name': 'OrderWeightPlugin', 'slug': 'mj-order-weight-plugin', 'version': '0.0.0'}]


### Please verify if you can reproduce this bug on the demo site.

- [ ] I can reproduce this bug on the demo site.

### Relevant log output

```shell
08:35:44 [Q] INFO Processed [foxtrot-fourteen-illinois-massachusetts]
08:35:45 [Q] INFO Processed [diet-mango-mirror-triple]
08:35:45 [Q] INFO Processed [fruit-kilo-sixteen-blossom]
08:35:45 [Q] INFO Processed [triple-washington-neptune-fruit]
08:35:45 [Q] INFO Processed [delaware-romeo-early-sierra]
08:35:45 [Q] INFO Processed [foxtrot-football-oven-stream]
08:35:45 [Q] INFO Processed [salami-football-failed-angel]
08:35:45 [Q] INFO Processed [glucose-louisiana-massachusetts-oxygen]
08:35:46 [Q] INFO Processed [indigo-october-two-mockingbird]
08:35:46 [Q] INFO Processed [kentucky-arizona-paris-seven]
08:35:46 [Q] INFO Processed [lactose-lamp-diet-whiskey]
08:35:46 [Q] INFO Processed [october-aspen-bacon-thirteen]
08:35:46 [Q] INFO Processed [lion-failed-oven-nine]
08:35:46 [Q] INFO Processed [quebec-oklahoma-alabama-jersey]
08:35:47 [Q] INFO Processed [nineteen-winter-september-bravo]
08:35:47 [Q] INFO Processed [oscar-rugby-snake-glucose]
08:35:47 [Q] INFO Processed [thirteen-oxygen-sink-carpet]
08:35:47 [Q] INFO Processed [magnesium-video-xray-double]
08:35:47 [Q] INFO Processed [oranges-sweet-maine-monkey]
08:35:47 [Q] INFO Processed [tango-whiskey-connecticut-ack]
08:35:47 [Q] INFO Processed [king-twelve-vegan-dakota]
08:35:47 [Q] INFO Processed [happy-wolfram-fanta-sodium]
08:35:48 [Q] INFO Processed [georgia-wolfram-fix-washington]
08:35:48 [Q] INFO Processed [yellow-arizona-colorado-oregon]
08:35:48 [Q] INFO Processed [massachusetts-lion-sodium-batman]
08:35:48 [Q] INFO Processed [mexico-georgia-tennessee-berlin]
08:35:48 [Q] INFO Processed [two-xray-idaho-don]
08:35:49 [Q] INFO Processed [princess-oxygen-nuts-oscar]
08:35:49 [Q] INFO Processed [oscar-asparagus-sodium-california]
08:35:49 [Q] INFO Processed [fix-crazy-paris-december]
08:35:49 [Q] INFO Processed [fish-paris-red-jig]
08:35:49 [Q] INFO Processed [ten-johnny-seventeen-august]
08:35:49 [Q] INFO Processed [ink-maine-bacon-spaghetti]
08:35:49 [Q] INFO Processed [cup-alaska-spaghetti-kitten]
08:35:50 [Q] INFO Processed [golf-harry-oven-october]
08:35:50 [Q] INFO Processed [burger-winter-foxtrot-august]
08:35:50 [Q] INFO Processed [twelve-hydrogen-cardinal-music]
08:35:50 [Q] INFO Processed [bacon-queen-xray-burger]
08:35:50 [Q] INFO Processed [east-floor-four-colorado]
08:35:50 [Q] INFO Processed [idaho-fifteen-fourteen-skylark]
08:35:51 [Q] INFO Processed [idaho-cola-september-delaware]
08:35:51 [Q] INFO Processed [comet-yellow-spaghetti-black]
08:35:51 [Q] INFO Processed [fourteen-carbon-sierra-saturn]
08:35:51 [Q] INFO Processed [venus-alabama-bulldog-butter]
08:35:51 [Q] INFO Processed [bacon-low-shade-mississippi]
08:35:51 [Q] INFO Processed [oregon-angel-grey-beer]
08:35:52 [Q] INFO Processed [nineteen-utah-lithium-emma]
08:35:52 [Q] INFO Processed [alpha-salami-enemy-whiskey]
08:35:52 [Q] INFO Processed [king-oven-fourteen-sixteen]
08:35:52 [Q] INFO Processed [pizza-lion-jig-thirteen]
08:35:52 [Q] INFO Processed [uranus-single-lamp-three]
08:35:53 [Q] INFO Processed [violet-maryland-potato-texas]
08:35:53 [Q] INFO Processed [sad-one-colorado-georgia]
08:35:53 [Q] INFO Processed [music-july-georgia-golf]
08:35:53 [Q] INFO Processed [sweet-four-thirteen-gee]
08:35:53 [Q] INFO Processed [green-snake-network-arizona]
08:35:53 [Q] INFO Processed [fanta-jupiter-maine-sixteen]
08:35:53 [Q] INFO Processed [oklahoma-may-lactose-twenty]
08:35:53 [Q] INFO Processed [wisconsin-orange-nineteen-bluebird]
08:35:53 [Q] INFO Processed [yankee-floor-item-floor]
08:35:53 [Q] INFO Processed [mexico-maryland-carolina-king]
08:35:54 [Q] INFO Processed [sixteen-washington-apart-juliet]
08:35:54 [Q] INFO Processed [saturn-uncle-michigan-white]
08:35:54 [Q] INFO Processed [blue-failed-mike-ohio]
08:35:54 [Q] INFO Processed [uniform-vermont-pizza-nebraska]
08:35:54 [Q] INFO Processed [high-magazine-red-spring]
08:35:55 [Q] INFO Processed [carpet-thirteen-sixteen-nuts]
08:35:55 [Q] INFO Processed [texas-magnesium-beryllium-magnesium]
08:35:55 [Q] INFO Processed [winner-ceiling-queen-april]
08:35:55 [Q] INFO Processed [california-oxygen-chicken-nuts]
08:35:55 [Q] INFO Processed [fix-connecticut-triple-beryllium]
08:35:55 [Q] INFO Processed [jig-ink-lamp-oven]
08:36:44 [Q] INFO Enqueued 33366
08:36:44 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:36:44 [Q] INFO Process-1:64 processing [iowa-comet-cold-virginia]
08:36:44 [Q] INFO Processed [iowa-comet-cold-virginia]
08:39:44 [Q] INFO Enqueued 33367
08:39:44 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:39:44 [Q] INFO Enqueued 33368
08:39:44 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:39:44 [Q] INFO Enqueued 33369
08:39:44 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:39:44 [Q] INFO Enqueued 33370
08:39:44 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:39:44 [Q] INFO Enqueued 33371
08:39:44 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:39:45 [Q] INFO Process-1:66 processing [nevada-kansas-magnesium-quiet]
08:39:45 [Q] INFO Process-1:67 processing [island-social-island-yellow]
08:39:45 [Q] INFO Process-1:65 processing [delaware-gee-india-hydrogen]
08:39:45 [Q] INFO Process-1:64 processing [delta-iowa-bluebird-massachusetts]
08:39:45 [Q] INFO Process-1:64 processing [mockingbird-fifteen-alpha-music]
08:39:45 [Q] INFO Processed [delta-iowa-bluebird-massachusetts]
08:39:45 [Q] INFO Processed [nevada-kansas-magnesium-quiet]
08:39:46 [Q] INFO Processed [island-social-island-yellow]
08:39:46 [Q] INFO Processed [delaware-gee-india-hydrogen]
08:39:46 [Q] INFO Processed [mockingbird-fifteen-alpha-music]
08:41:45 [Q] INFO Enqueued 33372
08:41:45 [Q] INFO Process-1 created a task from schedule [InvenTree.tasks.heartbeat]
08:41:45 [Q] INFO Process-1:66 processing [red-arkansas-burger-oregon]
08:41:45 [Q] INFO Processed [red-arkansas-burger-oregon]
```



Discussion:
I tried to check if it's same as #4179 but I cannot tell....
@lippoliv thanks for reporting. You should be able to determine which are the guilty tasks here by looking under "successful tasks" in the admin interface 
Maybe this is also related to the currently down exchange api?
@SchrodingersGat Good hint, thanks. The majority is `plugin.base.event.events.process_event`. Any chance to boil it down even more?

<img width="936" alt="image" src="https://github.com/inventree/InvenTree/assets/2561859/aeede3a9-a27b-44f0-8de1-3d7e0d16aa47">

⚠️ 🥺 🆘  This is getting a serious issue. While yesterday it was on "shipping", now only allocating stock for sale orders is spawning hundrets of jobs.

I have now 200 sale orders, 5 build orders, 28 parts and currently 27 stock items in stock.

48 orders are to be processed, I assigned stock for 5 of them, now I sit here waiting for averaging 700 background jobs for 15 minutes or so :/

But now it's a mix between `part.tasks.update_part_pricing` and `plugin.base.event.events.process_event`
<img width="935" alt="image" src="https://github.com/inventree/InvenTree/assets/2561859/a9ad61f2-43c6-4b2b-bf68-b4d3f5039475">


<details>
  <summary>Part pricing recursive? (less important after update, expand for detials)</summary>


I researched a little more and in admin panel for `part.tasks.update_part_pricing` I saw that there's some `counter` which is at 23. Reviewing the code, this `counter` is defined as

> How many times this function has been called in sequence

Why would `update_part_pricing` be called sequencially?

<img width="496" alt="image" src="https://github.com/inventree/InvenTree/assets/2561859/c7910d71-5084-4924-a1cb-f331c1076571">



### Loop discovered?

```python
class PartPricing(common.models.MetaMixin):
    ...

    def update_pricing(self, counter: int = 0, cascade: bool = True):
       ...

        if counter > 25:
            # Prevent infinite recursion / stack depth issues
            logger.debug(counter, f"Skipping pricing update for {p} - maximum depth exceeded")
            return
       ...

        # Update parent assemblies and templates
        if cascade:
            self.update_assemblies(counter)
            self.update_templates(counter)
```

I guess this is a problem. At least for me:

I have a Part which is a template but has it's own Stock and can be sold. One of it's variants is a modification, so in the BOM I referenced the Part as required.

Now if we trigger `update_pricing` for the variant, it will trigger it for the template. And then on executing the event for the template, it will trigger the variant again, right? Happily there's the counter in place to cancle the endless loop, but they are running in parallel, maybe, since multiple sales orders are worked on in parallel?

</details>


### "fix" price calculation loop problem

I removed the link from the variants to the parents. It's 2023-09-23 12:00 UTC +2, queue is at 1500 jobs (and was "stable" at this amount for roughly one hour).

I checked back 2023-09-23 12:35 UTC +2, queue is at 900 jobs, so it seems to work.

### Trigger hundrets of background jobs easily

The above said, I think here's what needs to be fixed. Here's a STR (steps to reproduce) at least for price calculation bottleneck:

**Setup**
Part 1: Template, Component
Part 2: Variant of Part 1, having Part 1 in BOM
Part 3: Having Part 2 in BOM
Part 4: Having Part 1 in BOM

**STR**
Trigger price calculation for Part 2

**Simulation**
1. (Part 2) Now in `update_pricing` we will queue one new job to update pricing for Part 1 (part template) and Part 3 (assemblies).
2. (Part 1, Counter 2) In `update_pricing` we will queue update pricing for Part 2 (assemblies)
3. (Part 2, Counter 3) NIn `update_pricing` we will queue update pricing for Part 1 (part template) and Part 3 (assemblies).
4. (Part 1, Counter 4) In `update_pricing` we will queue update pricing for Part 2 (assemblies)
5. (Part 2, Counter 5) NIn `update_pricing` we will queue update pricing for Part 1 (part template) and Part 3 (assemblies).
6. ...

**Questions arise**
1. Why do we even `update_pricing`, I'm pretty sure pricing for the part doesn't change. Fair enough, there's a new sale order, but no argument to execute a loop, right?
2. Even if there's a reason for 1, we probably should stop the loop, if just sale cost did change. That will not have any impact on assembiles nor on templates, right?
3. If we want to queue a price update for one Part, can we "skip" it, if it's queued already?

This is not 100% of my problems, I'm pretty sure, but at least I can go back to using variants :)
I analyzed this now and over the last 1:45 hours I updates my comment multiple times, now I stop updating it, waiting for some DEV feedback (since I'm more or less guessing, based on what I see in code).
The larger problem here is that a simple process_event is taking multiple seconds. As a comparison: A production docker setup needs around 450ms on my MacBook Air M2 for this task.

You did not fill out the version information as described in the template so we do have not all the info we need. The long process times are probably due to IO constraints, most often we see this with tight RAM allocation or low-end SoC's in NAS systems. How much RAM and CPU cores do you have assigned to the worker?
The pricing thing needs a deeper investigation, which will have to wait till the main devs are back in the office - this is a spare-time project after all. I would appreciate less alarmist language and emojis in general.
@matmair agreed, I should have removed the emojis after I found the workaround to reduce urgency, sorry that I missed that.

> The larger problem here is that a simple process_event is taking multiple seconds. 

Isn't it shown in milliseconds, so 26 ms not 26 seconds, right?
At least in the log output I see a couple per second (not 40, but 1 - 4 I would say).

> You did not fill out the version information as described in the template so we do have not all the info we need

Sorry I overread the small sentence :/
I updated the information, hopefully it's complete now.

> How much RAM and CPU cores do you have assigned to the worker?

I use the "default" docker compose file (https://github.com/inventree/InvenTree/blob/master/docker-compose.yml), where there are no limits applied. The system itself has 8 GiB RAM with 4 * 1,5 GHz (Synology DS918+, RAM upgraded).

Is there some limits by default in the config, which I should override?

> The pricing thing needs a deeper investigation

Should I split it into a standalone bug and try to create a "0 to bug" steps to reproduce (empty installation with all needed steps)? So then at least the pricing thing has a clear playground and can be worked on?

At least that thing was not why I creatd this bug yesterday. It came up after I introduced some new Part's (and links) to the system.
@lippoliv can you look into the "process_event" tasks - it should include the name of the event being processed in each case. Maybe there are some further clues as to more improvements we can make :)
Is there a way to run a database query or so? I just see details in "successful events" and need to open them manually.

Here are some screenshots, but just "random" once

![Xnip2023-09-24_15-06-51](https://github.com/inventree/InvenTree/assets/2561859/47885328-1491-4164-8fbc-c955dee9e365)
![Xnip2023-09-24_15-07-06](https://github.com/inventree/InvenTree/assets/2561859/4f22576a-d526-40c7-b79c-a0de0f6289e2)
![Xnip2023-09-24_15-07-19](https://github.com/inventree/InvenTree/assets/2561859/f4b7823a-8765-43c6-b753-9d9823eb57ee)
![Xnip2023-09-24_15-09-37](https://github.com/inventree/InvenTree/assets/2561859/1d99cdc4-b128-41c5-8ab2-b199950e38f1)

so when a sales order is completed, or the items are shipped, then there are a *lot* of events that will be triggered based on that, such as:

- Sales order update
- Stock item updates
- sales order allocation updates
- shipping updates

(etc etc etc)

These events fire so that any plugins can make use of them. However, if plugins are *not* making use of them, then each event "wastes" a worker cycle. Typically a cycle is 1 second or so, minimum, no matter how trivial the event task actually is.

One potential way we could improve this is by asking each plugin which events they are interested in (at the start, when plugins are loaded) and build out a set of "registered" events which we know plugins might want to respond to. Any other events can be ignored and not even pushed to the background worker threads.

This would be a breaking change, unless there is a way we can seamlessly transition to this approach. But it should cut a lot of work from the background process.

@matmair thoughts on this? Or an alternative approach?
OK that's good point probably. I have two plugins listening to events. Their logic is "guarded", but if they get all possible events and per event it takes one second, that's still pretty bad.

**Example:**
```python
    def process_event(self, event, *args, **kwargs):
        if event != "salesordershipment.completed":
            return
```

So probably for me right now there's not much I can do except disabling this plugins?
> So probably for me right now there's not much I can do except disabling this plugins?

I'm not actually sure that will improve things anyway - the event is still registered in the system, even if there are no plugins which actually want to use it.
OK let's see what other DEVs say. If I have some spare time the next days, maybe I can dig deeper again especially on this situation. Cannot promise anything, but I'll try.

Resolving patch:
diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py
--- a/InvenTree/plugin/base/event/events.py
+++ b/InvenTree/plugin/base/event/events.py
@@ -65,27 +65,32 @@ def register_event(event, *args, **kwargs):
         with transaction.atomic():
 
             for slug, plugin in registry.plugins.items():
-
-                if plugin.mixin_enabled('events'):
-
-                    if plugin.is_active():
-                        # Only allow event registering for 'active' plugins
-
-                        logger.debug("Registering callback for plugin '%s'", slug)
-
-                        # This task *must* be processed by the background worker,
-                        # unless we are running CI tests
-                        if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS:
-                            kwargs['force_async'] = True
-
-                        # Offload a separate task for each plugin
-                        offload_task(
-                            process_event,
-                            slug,
-                            event,
-                            *args,
-                            **kwargs
-                        )
+                if not plugin.mixin_enabled('events'):
+                    continue
+
+                # Only allow event registering for 'active' plugins
+                if not plugin.is_active():
+                    continue
+
+                # Let the plugin decide if it wants to process this event
+                if not plugin.wants_process_event(event):
+                    continue
+
+                logger.debug("Registering callback for plugin '%s'", slug)
+
+                # This task *must* be processed by the background worker,
+                # unless we are running CI tests
+                if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS:
+                    kwargs['force_async'] = True
+
+                # Offload a separate task for each plugin
+                offload_task(
+                    process_event,
+                    slug,
+                    event,
+                    *args,
+                    **kwargs
+                )
 
 
 def process_event(plugin_slug, event, *args, **kwargs):
diff --git a/InvenTree/plugin/base/event/mixins.py b/InvenTree/plugin/base/event/mixins.py
--- a/InvenTree/plugin/base/event/mixins.py
+++ b/InvenTree/plugin/base/event/mixins.py
@@ -9,6 +9,15 @@ class EventMixin:
     Implementing classes must provide a "process_event" function:
     """
 
+    def wants_process_event(self, event):
+        """Function to subscribe to events.
+
+        Return true if you're interested in the given event, false if not.
+        """
+
+        # Default implementation always returns true (backwards compatibility)
+        return True
+
     def process_event(self, event, *args, **kwargs):
         """Function to handle events.
 
diff --git a/InvenTree/plugin/samples/event/filtered_event_sample.py b/InvenTree/plugin/samples/event/filtered_event_sample.py
new file mode 100644
--- /dev/null
+++ b/InvenTree/plugin/samples/event/filtered_event_sample.py
@@ -0,0 +1,32 @@
+"""Sample plugin which responds to events."""
+
+import logging
+
+from django.conf import settings
+
+from plugin import InvenTreePlugin
+from plugin.mixins import EventMixin
+
+logger = logging.getLogger('inventree')
+
+
+class FilteredEventPluginSample(EventMixin, InvenTreePlugin):
+    """A sample plugin which provides supports for triggered events."""
+
+    NAME = "FilteredEventPlugin"
+    SLUG = "filteredsampleevent"
+    TITLE = "Triggered by test.event only"
+
+    def wants_process_event(self, event):
+        """Return whether given event should be processed or not."""
+        return event == "test.event"
+
+    def process_event(self, event, *args, **kwargs):
+        """Custom event processing."""
+        print(f"Processing triggered event: '{event}'")
+        print("args:", str(args))
+        print("kwargs:", str(kwargs))
+
+        # Issue warning that we can test for
+        if settings.PLUGIN_TESTING:
+            logger.debug('Event `%s` triggered in sample plugin', event)


Test patch:
diff --git a/InvenTree/plugin/samples/event/test_filtered_event_sample.py b/InvenTree/plugin/samples/event/test_filtered_event_sample.py
new file mode 100644
--- /dev/null
+++ b/InvenTree/plugin/samples/event/test_filtered_event_sample.py
@@ -0,0 +1,52 @@
+"""Unit tests for event_sample sample plugins."""
+
+from django.conf import settings
+from django.test import TestCase
+
+from common.models import InvenTreeSetting
+from plugin import registry
+from plugin.base.event.events import trigger_event
+
+from .filtered_event_sample import logger
+
+
+class FilteredEventPluginSampleTests(TestCase):
+    """Tests for EventPluginSample."""
+
+    def test_run_event(self):
+        """Check if the event is issued."""
+        # Activate plugin
+        config = registry.get_plugin('filteredsampleevent').plugin_config()
+        config.active = True
+        config.save()
+
+        InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None)
+
+        # Enable event testing
+        settings.PLUGIN_TESTING_EVENTS = True
+        # Check that an event is issued
+        with self.assertLogs(logger=logger, level="DEBUG") as cm:
+            trigger_event('test.event')
+        self.assertIn('DEBUG:inventree:Event `test.event` triggered in sample plugin', cm[1])
+
+        # Disable again
+        settings.PLUGIN_TESTING_EVENTS = False
+
+    def test_ignore_event(self):
+        """Check if the event is issued."""
+        # Activate plugin
+        config = registry.get_plugin('filteredsampleevent').plugin_config()
+        config.active = True
+        config.save()
+
+        InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None)
+
+        # Enable event testing
+        settings.PLUGIN_TESTING_EVENTS = True
+        # Check that an event is issued
+        with self.assertLogs(logger=logger, level="DEBUG") as cm:
+            trigger_event('test.some.other.event')
+        self.assertNotIn('DEBUG:inventree:Event `test.some.other.event` triggered in sample plugin', cm[1])
+
+        # Disable again
+        settings.PLUGIN_TESTING_EVENTS = False