Skip to content

Commit db49d45

Browse files
committed
fix production mode and scheduler race
1 parent 7c5a137 commit db49d45

File tree

4 files changed

+77
-25
lines changed

4 files changed

+77
-25
lines changed

appdaemon/app_management.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -163,28 +163,23 @@ def sequence_config(self) -> SequenceConfig | None:
163163
def valid_apps(self) -> set[str]:
164164
return self.running_apps | self.loaded_globals
165165

166-
def start(self) -> None:
167-
"""Start the app management subsystem, which creates async tasks to
168-
169-
* Initialize admin entities
170-
* Call :meth:`~.check_app_updates`
171-
* Fire an ``appd_started`` event in the ``global`` namespace.
166+
async def start(self) -> None:
167+
"""Start the app management subsystem.
172168
169+
This method:
170+
* Initializes the dependency manager (INIT mode)
171+
* Loads all apps (normal mode)
172+
* Initializes admin entities
173173
"""
174174
if self.AD.apps_enabled:
175-
self.logger.debug("Starting the app management subsystem")
176-
self.AD.loop.create_task(self.init_admin_entities())
175+
self.logger.debug("Initializing app system")
176+
await self.check_app_updates(mode=UpdateMode.INIT)
177177

178-
task = self.AD.loop.create_task(
179-
self.check_app_updates(mode=UpdateMode.INIT),
180-
name="check_app_updates",
181-
)
182-
task.add_done_callback(
183-
lambda _: self.AD.loop.create_task(
184-
self.AD.events.process_event("global", {"event_type": "appd_started", "data": {}}),
185-
name="appd_started_event"
186-
)
187-
)
178+
self.logger.debug("Loading apps")
179+
await self.check_app_updates()
180+
181+
await self.init_admin_entities()
182+
self.logger.info("App initialization complete")
188183

189184
async def stop(self) -> None:
190185
"""Stop the app management subsystem and all the running apps.

appdaemon/appdaemon.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,20 +367,15 @@ def start(self) -> None:
367367
"""Start AppDaemon, which also starts all the component subsystems like the scheduler, etc.
368368
369369
- :meth:`ThreadAsync <appdaemon.thread_async.ThreadAsync.start>`
370-
- :meth:`Scheduler <appdaemon.scheduler.Scheduler.start>`
371370
- :meth:`Utility <appdaemon.utility_loop.Utility.start>`
372-
- :meth:`AppManagement <appdaemon.app_management.AppManagement.start>`
373371
372+
Note: The scheduler is started by the utility loop after plugins are ready.
374373
"""
375374
self.logger.debug("Starting AppDaemon")
376375
self.thread_async.start()
377-
self.sched.start()
378376
self.utility.start()
379377
self.state.start()
380378

381-
if self.apps_enabled:
382-
self.app_management.start()
383-
384379
async def stop(self) -> None:
385380
"""Stop AppDaemon by calling the stop method of the subsystems.
386381

appdaemon/utility_loop.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ async def _init_loop(self):
143143
* Starts the web server if configured
144144
* Waits for all plugins to initialize
145145
* Registers services
146-
* Runs check_app_updates with UpdateMode.INIT if apps are enabled
146+
* Starts the scheduler
147+
* Initializes apps if apps are enabled
147148
"""
148149
self.logger.debug("Starting utility loop")
149150

@@ -158,8 +159,21 @@ async def _init_loop(self):
158159
# Wait for all plugins to initialize
159160
await self.AD.plugins.wait_for_plugins()
160161

162+
if self.AD.stopping:
163+
self.logger.debug("AppDaemon already stopping before starting utility loop")
164+
return
165+
161166
await self._register_services()
162167

168+
# Start the scheduler
169+
self.AD.sched.start()
170+
171+
if self.AD.apps_enabled:
172+
await self.AD.app_management.start()
173+
174+
# Fire APPD Started Event
175+
await self.AD.events.process_event("global", {"event_type": "appd_started", "data": {}})
176+
163177
async def loop(self):
164178
"""Run the utility loop, which handles the following:
165179
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
from unittest.mock import AsyncMock
3+
4+
import pytest
5+
import pytest_asyncio
6+
from appdaemon.appdaemon import AppDaemon
7+
8+
9+
@pytest_asyncio.fixture(scope="function")
10+
async def ad_production(ad_obj: AppDaemon):
11+
"""AppDaemon fixture with production_mode enabled."""
12+
ad_obj.config.production_mode = True
13+
ad_obj.app_dir = ad_obj.config_dir / "apps/hello_world"
14+
15+
ad_obj.start()
16+
yield ad_obj
17+
await ad_obj.stop()
18+
19+
20+
@pytest.mark.ci
21+
@pytest.mark.functional
22+
@pytest.mark.asyncio(loop_scope="session")
23+
async def test_production_mode_loads_apps(ad_production: AppDaemon) -> None:
24+
"""Test that apps load correctly when production_mode is enabled."""
25+
# Wait for initialization to complete
26+
await ad_production.utility.app_update_event.wait()
27+
# Check that the app loaded
28+
assert "hello_world" in ad_production.app_management.objects
29+
30+
31+
@pytest.mark.ci
32+
@pytest.mark.functional
33+
@pytest.mark.asyncio(loop_scope="session")
34+
async def test_production_mode_no_reloading(ad_production: AppDaemon) -> None:
35+
"""Test that production mode doesn't reload apps when files change."""
36+
# Wait for initialization to complete
37+
await ad_production.utility.app_update_event.wait()
38+
39+
# Mock check_app_updates to track calls from now on
40+
mock = AsyncMock(wraps=ad_production.app_management.check_app_updates)
41+
ad_production.app_management.check_app_updates = mock
42+
43+
# Touch file and wait for utility loop
44+
ad_production.utility.app_update_event.clear()
45+
os.utime(ad_production.app_dir / "hello.py", None)
46+
await ad_production.utility.app_update_event.wait()
47+
48+
assert not mock.called, "Should not reload in production mode"

0 commit comments

Comments
 (0)