From a46e612cbe9fb4a16a016221fe386ea4edb9648a Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 11 Nov 2025 11:29:13 +0100 Subject: [PATCH 1/4] fix: Fix detection of whether any instance is initialized Closes: #674 --- src/apify/_actor.py | 16 ++++++++++++---- tests/unit/conftest.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 0e175c31..dae1fb56 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -98,8 +98,8 @@ async def main() -> None: _is_rebooting = False """Whether the Actor is currently rebooting.""" - _is_any_instance_initialized = False - """Whether any Actor instance was initialized.""" + _initialized_instance_count = 0 + """Count of currently initialized Actor instances.""" def __init__( self, @@ -175,7 +175,7 @@ async def __aenter__(self) -> Self: self.log.debug('Configuration initialized') # Warn about non-standard usage patterns. - if _ActorType._is_any_instance_initialized: + if self._is_any_instance_initialized: self.log.warning('Repeated Actor initialization detected - this is non-standard usage, proceed with care.') # Update the global Actor proxy to refer to this instance. @@ -197,7 +197,7 @@ async def __aenter__(self) -> Self: # Mark initialization as complete and update global state. self._is_initialized = True - _ActorType._is_any_instance_initialized = True + _ActorType._initialized_instance_count += 1 return self async def __aexit__( @@ -247,6 +247,9 @@ async def finalize() -> None: await asyncio.wait_for(finalize(), self._cleanup_timeout.total_seconds()) self._is_initialized = False + # Update global state - decrement instance count (ensure it doesn't go negative) + _ActorType._initialized_instance_count = max(0, _ActorType._initialized_instance_count - 1) + if self._exit_process: sys.exit(self.exit_code) @@ -389,6 +392,11 @@ def _storage_client(self) -> SmartApifyStorageClient: 'awaiting `Actor.init`.' ) + @property + def _is_any_instance_initialized(self) -> bool: + """Whether any Actor instance is currently initialized.""" + return self._initialized_instance_count > 0 + async def init(self) -> None: """Initialize the Actor without using context-manager syntax. diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index bd041b50..94bfad04 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -62,7 +62,7 @@ def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callabl def _prepare_test_env() -> None: if hasattr(apify._actor.Actor, '__wrapped__'): delattr(apify._actor.Actor, '__wrapped__') - apify._actor._ActorType._is_any_instance_initialized = False + apify._actor._ActorType._initialized_instance_count = 0 # Set the environment variable for the local storage directory to the temporary path. monkeypatch.setenv(ApifyEnvVars.LOCAL_STORAGE_DIR, str(tmp_path)) From 4c529a6ef7620cf13c0d82175a7309a089ec3fc7 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 11 Nov 2025 13:51:22 +0100 Subject: [PATCH 2/4] only boolean flag --- src/apify/_actor.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index dae1fb56..504cdb08 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -34,9 +34,8 @@ from apify._utils import docs_group, docs_name, get_system_info, is_running_in_ipython from apify.events import ApifyEventManager, EventManager, LocalEventManager from apify.log import _configure_logging, logger -from apify.storage_clients import ApifyStorageClient +from apify.storage_clients import ApifyStorageClient, SmartApifyStorageClient from apify.storage_clients._file_system import ApifyFileSystemStorageClient -from apify.storage_clients._smart_apify._storage_client import SmartApifyStorageClient from apify.storages import Dataset, KeyValueStore, RequestQueue if TYPE_CHECKING: @@ -98,8 +97,8 @@ async def main() -> None: _is_rebooting = False """Whether the Actor is currently rebooting.""" - _initialized_instance_count = 0 - """Count of currently initialized Actor instances.""" + _is_any_instance_initialized = False + """Whether any Actor instance is currently initialized.""" def __init__( self, @@ -197,7 +196,7 @@ async def __aenter__(self) -> Self: # Mark initialization as complete and update global state. self._is_initialized = True - _ActorType._initialized_instance_count += 1 + _ActorType._is_any_instance_initialized = True return self async def __aexit__( @@ -246,9 +245,7 @@ async def finalize() -> None: await asyncio.wait_for(finalize(), self._cleanup_timeout.total_seconds()) self._is_initialized = False - - # Update global state - decrement instance count (ensure it doesn't go negative) - _ActorType._initialized_instance_count = max(0, _ActorType._initialized_instance_count - 1) + _ActorType._is_any_instance_initialized = False if self._exit_process: sys.exit(self.exit_code) @@ -392,11 +389,6 @@ def _storage_client(self) -> SmartApifyStorageClient: 'awaiting `Actor.init`.' ) - @property - def _is_any_instance_initialized(self) -> bool: - """Whether any Actor instance is currently initialized.""" - return self._initialized_instance_count > 0 - async def init(self) -> None: """Initialize the Actor without using context-manager syntax. From e57451825aebb3e15db265354680eb20819fe050 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 11 Nov 2025 13:52:39 +0100 Subject: [PATCH 3/4] . --- src/apify/_actor.py | 2 +- tests/unit/conftest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 504cdb08..5fe981ea 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -174,7 +174,7 @@ async def __aenter__(self) -> Self: self.log.debug('Configuration initialized') # Warn about non-standard usage patterns. - if self._is_any_instance_initialized: + if _ActorType._is_any_instance_initialized: self.log.warning('Repeated Actor initialization detected - this is non-standard usage, proceed with care.') # Update the global Actor proxy to refer to this instance. diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 94bfad04..bd041b50 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -62,7 +62,7 @@ def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callabl def _prepare_test_env() -> None: if hasattr(apify._actor.Actor, '__wrapped__'): delattr(apify._actor.Actor, '__wrapped__') - apify._actor._ActorType._initialized_instance_count = 0 + apify._actor._ActorType._is_any_instance_initialized = False # Set the environment variable for the local storage directory to the temporary path. monkeypatch.setenv(ApifyEnvVars.LOCAL_STORAGE_DIR, str(tmp_path)) From cd8cda3708567685ec16c86b04be83e02636cb56 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 12 Nov 2025 09:50:13 +0100 Subject: [PATCH 4/4] Only one is instance init field --- src/apify/_actor.py | 26 +++----- tests/unit/actor/test_actor_log.py | 101 ++++++++++++++--------------- tests/unit/conftest.py | 2 +- 3 files changed, 59 insertions(+), 70 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 5fe981ea..3fc99128 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -94,12 +94,6 @@ async def main() -> None: ``` """ - _is_rebooting = False - """Whether the Actor is currently rebooting.""" - - _is_any_instance_initialized = False - """Whether any Actor instance is currently initialized.""" - def __init__( self, configuration: Configuration | None = None, @@ -138,8 +132,14 @@ def __init__( self._apify_client: ApifyClientAsync | None = None - self._is_exiting = False self._is_initialized = False + """Whether any Actor instance is currently initialized.""" + + self._is_rebooting = False + """Whether the Actor is currently rebooting.""" + + self._is_exiting = False + """Whether the Actor is currently exiting.""" async def __aenter__(self) -> Self: """Enter the Actor context. @@ -168,15 +168,11 @@ async def __aenter__(self) -> Self: # Configure logging based on the configuration, any logs before this point are lost. if self._configure_logging: _configure_logging() - self.log.debug('Logging configured') + self.log.debug('Logging configured') self.log.info('Initializing Actor', extra=get_system_info()) self.log.debug('Configuration initialized') - # Warn about non-standard usage patterns. - if _ActorType._is_any_instance_initialized: - self.log.warning('Repeated Actor initialization detected - this is non-standard usage, proceed with care.') - # Update the global Actor proxy to refer to this instance. cast('Proxy', Actor).__wrapped__ = self self._is_exiting = False @@ -196,7 +192,6 @@ async def __aenter__(self) -> Self: # Mark initialization as complete and update global state. self._is_initialized = True - _ActorType._is_any_instance_initialized = True return self async def __aexit__( @@ -245,7 +240,6 @@ async def finalize() -> None: await asyncio.wait_for(finalize(), self._cleanup_timeout.total_seconds()) self._is_initialized = False - _ActorType._is_any_instance_initialized = False if self._exit_process: sys.exit(self.exit_code) @@ -1113,11 +1107,11 @@ async def reboot( self.log.error('Actor.reboot() is only supported when running on the Apify platform.') return - if _ActorType._is_rebooting: + if self._is_rebooting: self.log.debug('Actor is already rebooting, skipping the additional reboot call.') return - _ActorType._is_rebooting = True + self._is_rebooting = True if not custom_after_sleep: custom_after_sleep = self.configuration.metamorph_after_sleep diff --git a/tests/unit/actor/test_actor_log.py b/tests/unit/actor/test_actor_log.py index a09f1d9d..7bff15b4 100644 --- a/tests/unit/actor/test_actor_log.py +++ b/tests/unit/actor/test_actor_log.py @@ -36,72 +36,67 @@ async def test_actor_logs_messages_correctly(caplog: pytest.LogCaptureFixture) - # Test that exception in Actor.main is logged with the traceback raise RuntimeError('Dummy RuntimeError') - # We skip the first entry, as it is related to the initialization of `lazy_object_proxy.Proxy` for `Actor`. - records = caplog.records[1:] + records = caplog.records - # Updated expected number of log records (additional debug messages added) - assert len(records) == 14 + # Expected number of log records + assert len(records) == 13 - # Record 0: Logging configured - assert records[0].levelno == logging.DEBUG - assert records[0].message == 'Logging configured' + # Record 0: Initializing Actor + assert records[0].levelno == logging.INFO + assert records[0].message == 'Initializing Actor' - # Record 1: Initializing Actor - assert records[1].levelno == logging.INFO - assert records[1].message == 'Initializing Actor' + # Record 1: Configuration initialized + assert records[1].levelno == logging.DEBUG + assert records[1].message == 'Configuration initialized' - # Record 2: Configuration initialized + # Record 2: Storage client initialized assert records[2].levelno == logging.DEBUG - assert records[2].message == 'Configuration initialized' + assert records[2].message == 'Storage client initialized' - # Record 3: Storage client initialized + # Record 3: Event manager initialized assert records[3].levelno == logging.DEBUG - assert records[3].message == 'Storage client initialized' + assert records[3].message == 'Event manager initialized' - # Record 4: Event manager initialized + # Record 4: Charging manager initialized assert records[4].levelno == logging.DEBUG - assert records[4].message == 'Event manager initialized' + assert records[4].message == 'Charging manager initialized' - # Record 5: Charging manager initialized + # Record 5: Debug message assert records[5].levelno == logging.DEBUG - assert records[5].message == 'Charging manager initialized' + assert records[5].message == 'Debug message' - # Record 6: Debug message - assert records[6].levelno == logging.DEBUG - assert records[6].message == 'Debug message' + # Record 6: Info message + assert records[6].levelno == logging.INFO + assert records[6].message == 'Info message' - # Record 7: Info message - assert records[7].levelno == logging.INFO - assert records[7].message == 'Info message' + # Record 7: Warning message + assert records[7].levelno == logging.WARNING + assert records[7].message == 'Warning message' - # Record 8: Warning message - assert records[8].levelno == logging.WARNING - assert records[8].message == 'Warning message' + # Record 8: Error message + assert records[8].levelno == logging.ERROR + assert records[8].message == 'Error message' - # Record 9: Error message + # Record 9: Exception message with traceback (ValueError) assert records[9].levelno == logging.ERROR - assert records[9].message == 'Error message' - - # Record 10: Exception message with traceback (ValueError) - assert records[10].levelno == logging.ERROR - assert records[10].message == 'Exception message' - assert records[10].exc_info is not None - assert records[10].exc_info[0] is ValueError - assert isinstance(records[10].exc_info[1], ValueError) - assert str(records[10].exc_info[1]) == 'Dummy ValueError' - - # Record 11: Multiline log message - assert records[11].levelno == logging.INFO - assert records[11].message == 'Multi\nline\nlog\nmessage' - - # Record 12: Actor failed with an exception (RuntimeError) - assert records[12].levelno == logging.ERROR - assert records[12].message == 'Actor failed with an exception' - assert records[12].exc_info is not None - assert records[12].exc_info[0] is RuntimeError - assert isinstance(records[12].exc_info[1], RuntimeError) - assert str(records[12].exc_info[1]) == 'Dummy RuntimeError' - - # Record 13: Exiting Actor - assert records[13].levelno == logging.INFO - assert records[13].message == 'Exiting Actor' + assert records[9].message == 'Exception message' + assert records[9].exc_info is not None + assert records[9].exc_info[0] is ValueError + assert isinstance(records[9].exc_info[1], ValueError) + assert str(records[9].exc_info[1]) == 'Dummy ValueError' + + # Record 10: Multiline log message + assert records[10].levelno == logging.INFO + assert records[10].message == 'Multi\nline\nlog\nmessage' + + # Record 11: Actor failed with an exception (RuntimeError) + assert records[11].levelno == logging.ERROR + assert records[11].message == 'Actor failed with an exception' + assert records[11].exc_info is not None + assert records[11].exc_info[0] is RuntimeError + assert isinstance(records[11].exc_info[1], RuntimeError) + assert str(records[11].exc_info[1]) == 'Dummy RuntimeError' + + # Record 12: Exiting Actor + assert records[12].levelno == logging.INFO + assert records[12].message == 'Exiting Actor' diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index bd041b50..cf7f007f 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -62,7 +62,7 @@ def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callabl def _prepare_test_env() -> None: if hasattr(apify._actor.Actor, '__wrapped__'): delattr(apify._actor.Actor, '__wrapped__') - apify._actor._ActorType._is_any_instance_initialized = False + apify._actor.Actor._is_initialized = False # Set the environment variable for the local storage directory to the temporary path. monkeypatch.setenv(ApifyEnvVars.LOCAL_STORAGE_DIR, str(tmp_path))