leaf-common 1.2.41__tar.gz → 1.2.43__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {leaf_common-1.2.41 → leaf_common-1.2.43}/PKG-INFO +1 -1
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/asyncio/asyncio_executor.py +2 -1
- leaf_common-1.2.43/leaf_common/asyncio/asyncio_executor_pool.py +313 -0
- leaf_common-1.2.43/leaf_common/asyncio/event_loop_factory.py +78 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common.egg-info/PKG-INFO +1 -1
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common.egg-info/SOURCES.txt +3 -0
- leaf_common-1.2.43/tests/asyncio/asyncio_executor_pool_gc_test.py +342 -0
- leaf_common-1.2.43/tests/asyncio/event_loop_factory_test.py +94 -0
- leaf_common-1.2.41/leaf_common/asyncio/asyncio_executor_pool.py +0 -134
- {leaf_common-1.2.41 → leaf_common-1.2.43}/.coveragerc +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/.flake8 +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/.github/workflows/publish.yml +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/.github/workflows/tests.yml +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/.gitignore +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/.pylintrc +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/CODE_OF_CONDUCT.md +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/CONTRIBUTING.md +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/LICENSE.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/Makefile +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/NOTICE.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/README.md +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/SECURITY.md +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/SUPPORT.md +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/build_scripts/copyright_config.json +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/build_scripts/run_flake8.sh +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/build_scripts/run_pylint.sh +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/build_scripts/source_available_copyright.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/asyncio/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/asyncio/async_to_sync_generator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/asyncio/asyncio_threadpool_executor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/asyncio/task_executor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/candidates/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/candidates/constants.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/candidates/representation_types.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/config_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/config_filter_chain.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/config_handler.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/dictionary_overlay.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/environment_defaults.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/file_of_class.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/resolver.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/config/resolver_util.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/evaluation/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/evaluation/component_evaluator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/evaluation/metrics_merger.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/composite_string_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/inclusionary_replacement_string_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/replacement_string_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/sanitizer_util.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/string_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/strip_whitespace_string_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/filters/tensorflow_field_name_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/candidate_metrics_provider.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/cmp_to_key.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/comparator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/fitness_objective.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/fitness_objectives.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/fitness_objectives_builder.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/fitness_objectives_from_config.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/metrics_based_individual_comparator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/metrics_provider.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/none_comparator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/number_comparator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/fitness/reversed_comparator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/log_utils/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/log_utils/message_type.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/log_utils/structured_message.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/logging/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/logging/example_logging_config.hocon +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/logging/logging_setup.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/logging/stream_to_logger.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/boolean_list_parser.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/boolean_parser.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/canonical_multi_config_parser.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/dictionary_extractor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/field_extractor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/list_parser.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/parser.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/parsers/string_parser.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/abstract_easy_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/composite_persistor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/easy_hocon_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/easy_json_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/easy_txt_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/easy/easy_yaml_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/README.md +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/abstract_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/hocon_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/json_gzip_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/json_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/null_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/override_file_extension_provider.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/persistence_factory.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/raw_bytes_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/reference_helper.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/simple_file_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/text_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/factory/yaml_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/interface/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/interface/persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/interface/persistence_mechanism.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/interface/persistor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/interface/restorer.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/mechanism/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/mechanism/abstract_persistence_mechanism.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/mechanism/local_file_persistence_mechanism.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/mechanism/persistence_mechanism_factory.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/mechanism/persistence_mechanisms.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/persistence/mechanism/s3_file_persistence_mechanism.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/composite_progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/iterating_progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/json_progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/status_dict_progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/progress/worker_progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/registry/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/registry/representation_file_extension_provider_registry.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/registry/representation_persistence_registry.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/registry/representation_serialization_format_registry.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/registry/self_identifying_restorer.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/config/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/config/rule_set_config_helper.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/condition.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/features.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/rule.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/rule_set.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/rule_set_binding.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/data/rules_constants.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/condition_evaluator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/rule_evaluator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/rule_metrics_merger.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/rule_set_binding_evaluator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/rule_set_evaluator.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/evaluation/rule_set_metrics_merger.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/persistence/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/persistence/rule_set_binding_file_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/persistence/rule_set_file_persistence.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/serialization/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/serialization/condition_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/serialization/rule_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/serialization/rule_set_binding_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/serialization/rule_set_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/representation/rule_based/serialization/rule_set_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/auth0_direct_service_accessor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/leaf_service_access.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/service_accessor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/service_accessor_factory.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/static_token_service_accessor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/service/vault_dynamic_token_service_accessor.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/vault/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/vault/github_vault_login.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/vault/kubernetes_vault_login.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/vault/token_vault_login.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/vault/vault_login.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/security/vault/vault_login_factory.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/buffered_gzip_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/chained_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/conversion_policy.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/gzip_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/hocon_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/json_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/raw_bytes_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/serialization_formats.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/text_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/format/yaml_serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/deserializer.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/file_extension_provider.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/reference_pruner.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/self_identifying_representation_error.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/serialization_format.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/interface/serializer.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/prep/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/prep/empty_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/prep/metrics_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/prep/pass_through_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/prep/pass_through_reference_pruner.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/prep/redactor_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/util/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/util/bytes_decoder.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/serialization/util/text_file_reader.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/abstract_service_session.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/async_abstract_service_session.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/async_grpc_client_retry.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/extension_packaging.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/grpc_channel_security.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/grpc_client_retry.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/grpc_metadata_util.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/population_response_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/population_session.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/population_session_factory.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/session/response_candidate_dictionary_converter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/time/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/time/time_util.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/time/timeout.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/time/timeout_reached_exception.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/utils/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/utils/async_atomic_counter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common/utils/atomic_counter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common.egg-info/dependency_links.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common.egg-info/requires.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/leaf_common.egg-info/top_level.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/pyproject.toml +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/requirements-build.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/requirements.txt +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/setup.cfg +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/async_test_helpers.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/asyncio_executor_metrics_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/asyncio_executor_not_started_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/asyncio_executor_pool_metrics_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/asyncio_executor_started_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/asyncio_threadpool_executor_metrics_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/callable_class.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/asyncio/sync_test_helpers.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/candidates/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/candidates/test_representation_types.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/config/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/config/dictionary_overlay_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/config/resolver_util_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/filters/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/filters/test_sanitizer_util.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/filters/test_tensorflow_field_name_filter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/fitness/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/fitness/fitness_objectives_from_config_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/fixtures/saved_rule_set.rules +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/fixtures/saved_rules_model.json +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/parsers/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/parsers/canonical_multi_config_parser_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/progress/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/progress/test_status_dict_progress_reporter.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/representation/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/representation/rule_based/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/representation/rule_based/test_condition.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/representation/rule_based/test_rule.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/representation/rule_based/test_rule_set.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/serialization/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/serialization/encoding/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/serialization/encoding/decode_text_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/serialization/encoding/text_file_reader_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/serialization/prep/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/serialization/prep/redactor_dictionary_converter_test.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/session/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/session/test_extension_packaging.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/time/__init__.py +0 -0
- {leaf_common-1.2.41 → leaf_common-1.2.43}/tests/time/timeout_test.py +0 -0
|
@@ -36,6 +36,7 @@ from asyncio import Future
|
|
|
36
36
|
from asyncio import Task
|
|
37
37
|
from concurrent import futures
|
|
38
38
|
|
|
39
|
+
from leaf_common.asyncio.event_loop_factory import EventLoopFactory
|
|
39
40
|
from leaf_common.asyncio.task_executor import TaskExecutor
|
|
40
41
|
from leaf_common.asyncio.asyncio_threadpool_executor import AsyncioThreadPoolExecutor
|
|
41
42
|
|
|
@@ -60,7 +61,7 @@ class AsyncioExecutor(TaskExecutor):
|
|
|
60
61
|
# We are going to start new thread for this Executor,
|
|
61
62
|
# so we need a new event loop bound to this particular thread:
|
|
62
63
|
self._threadpool_executor: AsyncioThreadPoolExecutor = AsyncioThreadPoolExecutor()
|
|
63
|
-
self._loop: AbstractEventLoop =
|
|
64
|
+
self._loop: AbstractEventLoop = EventLoopFactory.new_event_loop()
|
|
64
65
|
self._loop.set_exception_handler(AsyncioExecutor.loop_exception_handler)
|
|
65
66
|
self._loop.set_default_executor(self._threadpool_executor)
|
|
66
67
|
self._loop_ready = threading.Event()
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
|
|
2
|
+
# Copyright © 2019-2026 Cognizant Technology Solutions Corp, www.cognizant.com.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# END COPYRIGHT
|
|
17
|
+
"""
|
|
18
|
+
See class comments
|
|
19
|
+
"""
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
from typing import Any
|
|
22
|
+
from typing import Dict
|
|
23
|
+
from typing import List
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
import copy
|
|
27
|
+
import logging
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
from leaf_common.asyncio.asyncio_executor import AsyncioExecutor
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AsyncioExecutorPool:
|
|
35
|
+
# pylint: disable=too-many-instance-attributes
|
|
36
|
+
"""
|
|
37
|
+
Class maintaining a dynamic set of reusable AsyncioExecutor instances.
|
|
38
|
+
|
|
39
|
+
Garbage collection policy
|
|
40
|
+
-------------------------
|
|
41
|
+
When reuse_mode is True, executors are returned to pool_available rather
|
|
42
|
+
than shut down. Each time an executor is returned, its return timestamp
|
|
43
|
+
is recorded.
|
|
44
|
+
|
|
45
|
+
Garbage collection is performed by a dedicated daemon GC thread that
|
|
46
|
+
runs on its own schedule, independent of any get_executor() /
|
|
47
|
+
return_executor() activity. Every gc_sweep_interval_seconds the thread
|
|
48
|
+
wakes, identifies any executor whose idle period exceeds
|
|
49
|
+
idle_timeout_seconds, removes it from pool_available, and shuts it
|
|
50
|
+
down. This keeps the caller of get_executor / return_executor off both
|
|
51
|
+
the sweep and the shutdown latency paths entirely.
|
|
52
|
+
|
|
53
|
+
Both the idle threshold and the sweep interval are internally
|
|
54
|
+
configurable via constructor parameters, with defaults given by
|
|
55
|
+
DEFAULT_IDLE_TIMEOUT_SECONDS and DEFAULT_GC_SWEEP_INTERVAL_SECONDS.
|
|
56
|
+
|
|
57
|
+
Lifecycle: callers should invoke shutdown() to stop the GC thread when
|
|
58
|
+
the pool is no longer needed. The GC thread holds a strong reference to
|
|
59
|
+
the pool, so omitting shutdown() will keep the pool (and its state) alive
|
|
60
|
+
for the lifetime of the process. The GC thread is a daemon so it will not
|
|
61
|
+
block process exit, but explicit shutdown is cleaner for tests and managed
|
|
62
|
+
long-running services.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# Internal configuration: default idle time (seconds) after which an
|
|
66
|
+
# executor sitting in pool_available is shut down and removed.
|
|
67
|
+
DEFAULT_IDLE_TIMEOUT_SECONDS: float = 3 * 60.0
|
|
68
|
+
|
|
69
|
+
# Internal configuration: default interval (seconds) at which the GC
|
|
70
|
+
# thread wakes to look for stale executors.
|
|
71
|
+
DEFAULT_GC_SWEEP_INTERVAL_SECONDS: float = 30.0
|
|
72
|
+
|
|
73
|
+
def __init__(self, reuse_mode: bool = True, *,
|
|
74
|
+
idle_timeout_seconds: float = DEFAULT_IDLE_TIMEOUT_SECONDS,
|
|
75
|
+
gc_sweep_interval_seconds: float = DEFAULT_GC_SWEEP_INTERVAL_SECONDS):
|
|
76
|
+
"""
|
|
77
|
+
Constructor.
|
|
78
|
+
:param reuse_mode: True, if requested executor instances
|
|
79
|
+
are taken from pool of available ones (pool mode);
|
|
80
|
+
False, if requested executor instances are created new
|
|
81
|
+
and shutdown on return (backward compatible mode)
|
|
82
|
+
Note that 2 parameters below are keyword-only, to make it more explicit when configuring the GC policy.
|
|
83
|
+
:param idle_timeout_seconds: Idle time threshold in seconds. An
|
|
84
|
+
AsyncioExecutor sitting in pool_available
|
|
85
|
+
longer than this is shut down and removed
|
|
86
|
+
by the background GC thread. Only applies
|
|
87
|
+
when reuse_mode is True.
|
|
88
|
+
:param gc_sweep_interval_seconds: Interval at which the GC thread
|
|
89
|
+
checks pool_available for stale executors.
|
|
90
|
+
Stale executors linger at most one sweep
|
|
91
|
+
interval beyond the idle timeout before
|
|
92
|
+
being collected. Only applies when
|
|
93
|
+
reuse_mode is True.
|
|
94
|
+
"""
|
|
95
|
+
self.reuse_mode: bool = reuse_mode
|
|
96
|
+
self.idle_timeout_seconds: float = idle_timeout_seconds
|
|
97
|
+
self.gc_sweep_interval_seconds: float = gc_sweep_interval_seconds
|
|
98
|
+
if self.reuse_mode:
|
|
99
|
+
if self.gc_sweep_interval_seconds <= 0:
|
|
100
|
+
raise ValueError("gc_sweep_interval_seconds must be > 0 when reuse_mode=True")
|
|
101
|
+
if self.idle_timeout_seconds < 0:
|
|
102
|
+
raise ValueError("idle_timeout_seconds must be >= 0 when reuse_mode=True")
|
|
103
|
+
# List of available (not currently used) AsyncioExecutor instances in the pool.
|
|
104
|
+
self.pool_available: List[AsyncioExecutor] = []
|
|
105
|
+
|
|
106
|
+
# List of currently used AsyncioExecutor instances in the pool.
|
|
107
|
+
self.pool_used: List[AsyncioExecutor] = []
|
|
108
|
+
|
|
109
|
+
# Maps id(executor) -> monotonic timestamp when the executor was
|
|
110
|
+
# returned to pool_available. Only contains entries for executors
|
|
111
|
+
# currently in pool_available; pruned on get_executor() and on sweep.
|
|
112
|
+
self._returned_at: Dict[int, float] = {}
|
|
113
|
+
|
|
114
|
+
self.lock = threading.Lock()
|
|
115
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
116
|
+
|
|
117
|
+
# Active GC: a daemon thread runs _gc_loop, sweeping on its own
|
|
118
|
+
# schedule. The stop event lets shutdown() interrupt the sweep
|
|
119
|
+
# interval wait promptly.
|
|
120
|
+
self._gc_stop_event: threading.Event = threading.Event()
|
|
121
|
+
self._gc_thread: Optional[threading.Thread] = None
|
|
122
|
+
if self.reuse_mode:
|
|
123
|
+
self._gc_thread = threading.Thread(
|
|
124
|
+
target=self._gc_loop,
|
|
125
|
+
name=f"AsyncioExecutorPool-GC-{id(self)}",
|
|
126
|
+
daemon=True,
|
|
127
|
+
)
|
|
128
|
+
self._gc_thread.start()
|
|
129
|
+
|
|
130
|
+
self.logger.debug(
|
|
131
|
+
"AsyncioExecutorPool created: %s reuse: %s idle_timeout: %.1fs sweep_interval: %.1fs",
|
|
132
|
+
id(self), str(self.reuse_mode), self.idle_timeout_seconds, self.gc_sweep_interval_seconds)
|
|
133
|
+
|
|
134
|
+
def get_executor(self) -> AsyncioExecutor:
|
|
135
|
+
"""
|
|
136
|
+
Get active (running) executor from the pool. Does not sweep;
|
|
137
|
+
sweeping is the GC thread's responsibility. If a stale executor
|
|
138
|
+
happens to sit at the head of pool_available before the next
|
|
139
|
+
sweep, get_executor() will reuse it -- which is fine, since
|
|
140
|
+
"stale" only means "would otherwise be collected as idle".
|
|
141
|
+
:return: AsyncioExecutor instance
|
|
142
|
+
"""
|
|
143
|
+
if self.reuse_mode:
|
|
144
|
+
with self.lock:
|
|
145
|
+
if len(self.pool_available) > 0:
|
|
146
|
+
result = self.pool_available.pop(0)
|
|
147
|
+
self._returned_at.pop(id(result), None)
|
|
148
|
+
self.logger.debug("Reusing AsyncioExecutor %s", id(result))
|
|
149
|
+
self.pool_used.append(result)
|
|
150
|
+
return result
|
|
151
|
+
# Create AsyncioExecutor outside of lock
|
|
152
|
+
# to avoid potentially longer locked periods
|
|
153
|
+
result = AsyncioExecutor()
|
|
154
|
+
result.start()
|
|
155
|
+
self.logger.debug("Creating AsyncioExecutor %s", id(result))
|
|
156
|
+
with self.lock:
|
|
157
|
+
self.pool_used.append(result)
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
def return_executor(self, executor: AsyncioExecutor):
|
|
161
|
+
"""
|
|
162
|
+
Return AsyncioExecutor instance back to the pool of available instances.
|
|
163
|
+
Does not sweep; the GC thread handles stale collection on its own
|
|
164
|
+
schedule.
|
|
165
|
+
:param executor: AsyncioExecutor to return.
|
|
166
|
+
"""
|
|
167
|
+
with self.lock:
|
|
168
|
+
if executor not in self.pool_used:
|
|
169
|
+
raise ValueError(f"Returned executor {id(executor)} is not in the pool of used executors")
|
|
170
|
+
self.pool_used.remove(executor)
|
|
171
|
+
|
|
172
|
+
# Executor clean up: cancel current tasks and shutdown if not in reuse mode.
|
|
173
|
+
if self.reuse_mode:
|
|
174
|
+
executor.cancel_current_tasks()
|
|
175
|
+
else:
|
|
176
|
+
self.logger.debug("Shutting down: AsyncioExecutor %s", id(executor))
|
|
177
|
+
executor.shutdown()
|
|
178
|
+
|
|
179
|
+
if self.reuse_mode:
|
|
180
|
+
with self.lock:
|
|
181
|
+
self.pool_available.append(executor)
|
|
182
|
+
self._returned_at[id(executor)] = time.monotonic()
|
|
183
|
+
self.logger.debug("Returned to pool: AsyncioExecutor %s pool size: %d",
|
|
184
|
+
id(executor), len(self.pool_available))
|
|
185
|
+
|
|
186
|
+
def shutdown(self, wait: bool = True) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Stop the background GC thread. After this returns, the pool no
|
|
189
|
+
longer reaps idle executors. Idempotent. Executors still held in
|
|
190
|
+
pool_used or pool_available are NOT shut down here; callers that
|
|
191
|
+
want those gone should handle them explicitly.
|
|
192
|
+
"""
|
|
193
|
+
with self.lock:
|
|
194
|
+
gc_thread = self._gc_thread
|
|
195
|
+
if gc_thread is None:
|
|
196
|
+
return
|
|
197
|
+
# Clear first to make concurrent shutdown() calls safe.
|
|
198
|
+
self._gc_thread = None
|
|
199
|
+
|
|
200
|
+
self._gc_stop_event.set()
|
|
201
|
+
if wait and threading.current_thread() is not gc_thread:
|
|
202
|
+
gc_thread.join()
|
|
203
|
+
|
|
204
|
+
def _gc_loop(self) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Active GC loop: sweep, then wait for either the configured
|
|
207
|
+
interval or the stop event, whichever comes first. Exits cleanly
|
|
208
|
+
when shutdown() sets the stop event.
|
|
209
|
+
|
|
210
|
+
Exceptions from _sweep_once are logged but do not stop the loop,
|
|
211
|
+
so a single bad executor cannot leak the rest of the pool.
|
|
212
|
+
"""
|
|
213
|
+
while not self._gc_stop_event.is_set():
|
|
214
|
+
try:
|
|
215
|
+
self._sweep_once()
|
|
216
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
217
|
+
self.logger.warning("GC: sweep raised %s; continuing", exc, exc_info=True)
|
|
218
|
+
# Sleep until the next sweep tick or until shutdown is signaled.
|
|
219
|
+
self._gc_stop_event.wait(timeout=self.gc_sweep_interval_seconds)
|
|
220
|
+
|
|
221
|
+
def _collect_executors(self, executors: Sequence[AsyncioExecutor]) -> None:
|
|
222
|
+
for executor in executors:
|
|
223
|
+
try:
|
|
224
|
+
self.logger.debug("GC: shutting down idle AsyncioExecutor %s", id(executor))
|
|
225
|
+
executor.shutdown(wait=True)
|
|
226
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
227
|
+
self.logger.warning(
|
|
228
|
+
"GC: shutdown failed for AsyncioExecutor %s: %s", id(executor), exc, exc_info=True)
|
|
229
|
+
|
|
230
|
+
def _sweep_once(self, now: Optional[float] = None) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Identify any executors in pool_available whose idle time exceeds
|
|
233
|
+
idle_timeout_seconds, remove them from the pool, and shut them
|
|
234
|
+
down. No-op when not in reuse mode.
|
|
235
|
+
|
|
236
|
+
pool_available is FIFO-ordered by return time (oldest at the head),
|
|
237
|
+
so the scan stops at the first non-stale entry.
|
|
238
|
+
|
|
239
|
+
Run by the GC thread on its periodic schedule; tests may call it
|
|
240
|
+
directly with a synthetic `now` for deterministic verification.
|
|
241
|
+
|
|
242
|
+
:param now: Optional override for the current time, in the same
|
|
243
|
+
units as time.monotonic(). Intended for tests; production
|
|
244
|
+
callers should omit this and let it default to
|
|
245
|
+
time.monotonic().
|
|
246
|
+
"""
|
|
247
|
+
if not self.reuse_mode:
|
|
248
|
+
return
|
|
249
|
+
if now is None:
|
|
250
|
+
now = time.monotonic()
|
|
251
|
+
to_collect: List[AsyncioExecutor] = []
|
|
252
|
+
with self.lock:
|
|
253
|
+
keep_from: int = 0
|
|
254
|
+
prev_returned_at: float = 0.0
|
|
255
|
+
for executor in self.pool_available:
|
|
256
|
+
if id(executor) not in self._returned_at:
|
|
257
|
+
# This should never happen, but log this and record executor
|
|
258
|
+
# as returned at the same time as the previous one - to keep sequence invariant still valid.
|
|
259
|
+
self.logger.warning(
|
|
260
|
+
"GC: executor %s in pool_available but missing return timestamp; fixing",
|
|
261
|
+
id(executor))
|
|
262
|
+
self._returned_at[id(executor)] = prev_returned_at
|
|
263
|
+
returned_at: float = self._returned_at.get(id(executor), prev_returned_at)
|
|
264
|
+
if now - returned_at > self.idle_timeout_seconds:
|
|
265
|
+
to_collect.append(executor)
|
|
266
|
+
keep_from += 1
|
|
267
|
+
prev_returned_at = returned_at
|
|
268
|
+
else:
|
|
269
|
+
break
|
|
270
|
+
if keep_from > 0:
|
|
271
|
+
for executor in self.pool_available[:keep_from]:
|
|
272
|
+
self._returned_at.pop(id(executor), None)
|
|
273
|
+
del self.pool_available[:keep_from]
|
|
274
|
+
# Shutdown outside the lock to keep the critical section short.
|
|
275
|
+
self._collect_executors(to_collect)
|
|
276
|
+
|
|
277
|
+
def get_threads_metrics(self) -> Dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Get metrics related to threads in the pool of executors:
|
|
280
|
+
for both collections of used and available executors,
|
|
281
|
+
get the total number of executors in this collection "executors",
|
|
282
|
+
the total number of work threads across all executors in this collection "work_threads",
|
|
283
|
+
and the total number of currently running work threads
|
|
284
|
+
across all executors in this collection "threads_running".
|
|
285
|
+
"""
|
|
286
|
+
with self.lock:
|
|
287
|
+
available_copy = copy.copy(self.pool_available)
|
|
288
|
+
used_copy = copy.copy(self.pool_used)
|
|
289
|
+
used_threads: int = 0
|
|
290
|
+
used_running: int = 0
|
|
291
|
+
available_threads: int = 0
|
|
292
|
+
available_running: int = 0
|
|
293
|
+
for executor in used_copy:
|
|
294
|
+
threads, running = executor.get_threads_metrics()
|
|
295
|
+
used_threads += threads
|
|
296
|
+
used_running += running
|
|
297
|
+
for executor in available_copy:
|
|
298
|
+
threads, running = executor.get_threads_metrics()
|
|
299
|
+
available_threads += threads
|
|
300
|
+
available_running += running
|
|
301
|
+
result_dict = {
|
|
302
|
+
"used": {
|
|
303
|
+
"executors": len(used_copy),
|
|
304
|
+
"work_threads": used_threads,
|
|
305
|
+
"threads_running": used_running
|
|
306
|
+
},
|
|
307
|
+
"available": {
|
|
308
|
+
"executors": len(available_copy),
|
|
309
|
+
"work_threads": available_threads,
|
|
310
|
+
"threads_running": available_running
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return result_dict
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
# Copyright © 2019-2026 Cognizant Technology Solutions Corp, www.cognizant.com.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# END COPYRIGHT
|
|
17
|
+
"""
|
|
18
|
+
See class comment for details.
|
|
19
|
+
"""
|
|
20
|
+
from typing import Callable
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
from asyncio import AbstractEventLoop
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EventLoopFactory:
|
|
30
|
+
"""
|
|
31
|
+
Factory for creating asyncio event loops with a platform-appropriate type.
|
|
32
|
+
|
|
33
|
+
On Windows the factory produces SelectorEventLoop instances (note: this loop
|
|
34
|
+
does not support asyncio subprocess APIs); on other platforms it defers to
|
|
35
|
+
asyncio's platform default. This is the forward-compatible replacement for the
|
|
36
|
+
asyncio.set_event_loop_policy() pattern (using asyncio.WindowsSelectorEventLoopPolicy
|
|
37
|
+
on Windows), which is deprecated in Python 3.14 and slated for removal.
|
|
38
|
+
|
|
39
|
+
Two entry points are provided so callers can pick what matches their
|
|
40
|
+
existing call site:
|
|
41
|
+
- loop_factory(): returns a callable suitable for the loop_factory=
|
|
42
|
+
keyword on asyncio.run() (Python 3.12+) and asyncio.Runner()
|
|
43
|
+
(Python 3.11+). Returns None on platforms where asyncio's
|
|
44
|
+
default is appropriate; None is the documented sentinel for
|
|
45
|
+
"use default", so the result can be passed through unchanged
|
|
46
|
+
on any platform.
|
|
47
|
+
- new_event_loop(): drop-in replacement for asyncio.new_event_loop()
|
|
48
|
+
at sites that construct a loop directly. Combine with
|
|
49
|
+
asyncio.set_event_loop() to install the loop for the current
|
|
50
|
+
thread before frameworks (e.g. Tornado) wrap it.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def loop_factory() -> Optional[Callable[[], AbstractEventLoop]]:
|
|
55
|
+
"""
|
|
56
|
+
:return: A callable that constructs a platform-appropriate event
|
|
57
|
+
loop, or None if asyncio's default is appropriate. None is
|
|
58
|
+
the documented value asyncio.run() and asyncio.Runner()
|
|
59
|
+
accept to mean "use the default factory", so the return
|
|
60
|
+
value of this method can be forwarded unchanged on any
|
|
61
|
+
platform.
|
|
62
|
+
"""
|
|
63
|
+
if sys.platform == "win32":
|
|
64
|
+
return asyncio.SelectorEventLoop
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def new_event_loop() -> AbstractEventLoop:
|
|
69
|
+
"""
|
|
70
|
+
:return: A newly-constructed event loop appropriate for the current
|
|
71
|
+
platform. On Windows this is a SelectorEventLoop; on other
|
|
72
|
+
platforms it is the loop type returned by
|
|
73
|
+
asyncio.new_event_loop().
|
|
74
|
+
"""
|
|
75
|
+
factory: Optional[Callable[[], AbstractEventLoop]] = EventLoopFactory.loop_factory()
|
|
76
|
+
if factory is not None:
|
|
77
|
+
return factory()
|
|
78
|
+
return asyncio.new_event_loop()
|
|
@@ -30,6 +30,7 @@ leaf_common/asyncio/async_to_sync_generator.py
|
|
|
30
30
|
leaf_common/asyncio/asyncio_executor.py
|
|
31
31
|
leaf_common/asyncio/asyncio_executor_pool.py
|
|
32
32
|
leaf_common/asyncio/asyncio_threadpool_executor.py
|
|
33
|
+
leaf_common/asyncio/event_loop_factory.py
|
|
33
34
|
leaf_common/asyncio/task_executor.py
|
|
34
35
|
leaf_common/candidates/__init__.py
|
|
35
36
|
leaf_common/candidates/constants.py
|
|
@@ -222,10 +223,12 @@ tests/asyncio/__init__.py
|
|
|
222
223
|
tests/asyncio/async_test_helpers.py
|
|
223
224
|
tests/asyncio/asyncio_executor_metrics_test.py
|
|
224
225
|
tests/asyncio/asyncio_executor_not_started_test.py
|
|
226
|
+
tests/asyncio/asyncio_executor_pool_gc_test.py
|
|
225
227
|
tests/asyncio/asyncio_executor_pool_metrics_test.py
|
|
226
228
|
tests/asyncio/asyncio_executor_started_test.py
|
|
227
229
|
tests/asyncio/asyncio_threadpool_executor_metrics_test.py
|
|
228
230
|
tests/asyncio/callable_class.py
|
|
231
|
+
tests/asyncio/event_loop_factory_test.py
|
|
229
232
|
tests/asyncio/sync_test_helpers.py
|
|
230
233
|
tests/candidates/__init__.py
|
|
231
234
|
tests/candidates/test_representation_types.py
|