SIENTIAPDE-988
Refactor OpcManager initialization and unsubscribe method; enhance logging - Removed the `initialize_from_config` method from `OpcManager` class and adjusted the constructor to handle configuration directly. - Improved the `unsubscribe` method to include detailed logging for non-existent subscriptions. - Updated tests in `test_opc_manager.py` to reflect changes in the `OpcManager` class. - Commented out Docker-related fixtures in `conftest.py` for potential future use. - Enhanced test coverage in `test_single_node.py` and `test_data_manager.py` with additional scenarios and assertions. - Introduced new methods in `Ingestor` and `IngestorManager` classes to manage server subscriptions and leases more effectively. - Added new tests for `Ingestor` class to validate initialization and slot management logic. - Implemented logging improvements across various classes to ensure better traceability of actions and errors.
This commit is contained in:
@@ -7,6 +7,19 @@ from kafka.errors import NoBrokersAvailable
|
||||
|
||||
class DataManager():
|
||||
def __init__(self, kafka_servers: str, logger: Logger) -> None:
|
||||
"""
|
||||
Initializes the DataManager instance with a Kafka producer.
|
||||
This constructor attempts to establish a connection to the specified Kafka servers
|
||||
and initializes a Kafka producer for sending messages. It retries the connection
|
||||
up to 3 times if the Kafka servers are unavailable.
|
||||
Args:
|
||||
kafka_servers (str): A comma-separated string of Kafka server addresses.
|
||||
logger (Logger): A logger instance for logging messages.
|
||||
Raises:
|
||||
NoBrokersAvailable: If the connection to Kafka servers fails after 3 attempts.
|
||||
"""
|
||||
|
||||
self.kafka_producer = None
|
||||
for i in range(0, 3):
|
||||
logger.info(
|
||||
f"Trying ({i}) to initializing DataManager with Kafka servers: {kafka_servers}")
|
||||
@@ -36,8 +49,11 @@ class DataManager():
|
||||
def __del__(self):
|
||||
"""Destructor to close the producer connection."""
|
||||
print("Closing Kafka producer...")
|
||||
self.kafka_producer.flush()
|
||||
self.kafka_producer.close()
|
||||
if self.kafka_producer:
|
||||
self.kafka_producer.flush(timeout=10)
|
||||
self.kafka_producer.close()
|
||||
else:
|
||||
print("Kafka producer is already closed or not initialized.")
|
||||
|
||||
def delivery_report(self, msg: str):
|
||||
"""Callback for delivery reports from Kafka."""
|
||||
|
||||
@@ -59,6 +59,29 @@ class IngestorManager():
|
||||
return manager
|
||||
|
||||
def update_opc_servers(self):
|
||||
"""
|
||||
Updates the OPC (OLE for Process Control) server connections managed by the ingestor.
|
||||
This method ensures that the OPC servers defined in `self.managed_tags` are properly
|
||||
initialized and updated. It performs the following tasks:
|
||||
- Registers new OPC servers based on the configuration in `self.managed_tags`.
|
||||
- Updates existing OPC server instances if their configuration has changed.
|
||||
- Disconnects and removes OPC servers that are no longer present in `self.managed_tags`.
|
||||
Steps:
|
||||
1. Iterates through the `self.managed_tags` dictionary to identify and register servers.
|
||||
2. Initializes new OPC server instances if they are not already managed.
|
||||
3. Reinitializes OPC server instances if their configuration has changed.
|
||||
4. Disconnects and removes OPC servers that are no longer registered.
|
||||
Attributes:
|
||||
self.managed_tags (dict): A nested dictionary containing slot and server configurations.
|
||||
self.opc_managers (dict): A dictionary mapping server names to their OPC manager instances.
|
||||
self.data_manager: An object responsible for managing data operations.
|
||||
self.logger: A logging object for recording warnings and other messages.
|
||||
Raises:
|
||||
Any exceptions raised during OPC server initialization or disconnection.
|
||||
Logs:
|
||||
- Warnings for servers that are no longer found in `self.managed_tags`.
|
||||
"""
|
||||
|
||||
registered_servers = []
|
||||
for slot, slot_config in self.managed_tags.items():
|
||||
for server, server_config in slot_config.items():
|
||||
@@ -91,18 +114,58 @@ class IngestorManager():
|
||||
self.opc_managers.pop(server, None)
|
||||
|
||||
def declare_active(self):
|
||||
"""
|
||||
Declares the ingestor as active by sending a heartbeat signal to the resource manager.
|
||||
This method ensures that the ingestor is marked as active by invoking the
|
||||
`ingestor_heartbeat` method of the associated resource manager.
|
||||
"""
|
||||
|
||||
self.resource_manager.ingestor_heartbeat()
|
||||
|
||||
def get_active_ingestors(self) -> List[str]:
|
||||
"""
|
||||
Retrieve a list of active ingestors.
|
||||
This method fetches all ingestors from the resource manager and returns them.
|
||||
If no ingestors are found, an empty list is returned.
|
||||
Returns:
|
||||
List[str]: A list of active ingestor names, or an empty list if none are found.
|
||||
"""
|
||||
|
||||
ingestors = self.resource_manager.get_all_ingestors()
|
||||
return ingestors if ingestors else []
|
||||
|
||||
def get_number_of_slots(self) -> int:
|
||||
"""
|
||||
Retrieves the number of slots managed by the resource manager.
|
||||
This method fetches all available slots from the resource manager,
|
||||
calculates their count, and updates the `number_of_slots` attribute.
|
||||
Returns:
|
||||
int: The total number of slots. Returns 0 if no slots are available.
|
||||
"""
|
||||
|
||||
slots = self.resource_manager.get_all_slots()
|
||||
self.number_of_slots = len(slots) if slots else 0
|
||||
return self.number_of_slots
|
||||
|
||||
def get_slot_leases(self, max_slots: int = 1) -> Dict:
|
||||
"""
|
||||
Acquires a specified number of resource slots by leasing them from the resource manager.
|
||||
Args:
|
||||
max_slots (int): The maximum number of slots to lease. Defaults to 1.
|
||||
Returns:
|
||||
Dict: A dictionary where the keys are the slot identifiers (as strings)
|
||||
and the values are the leased slot details.
|
||||
Behavior:
|
||||
- Iterates through available slots and attempts to lease them using the resource manager.
|
||||
- Logs the leasing of each slot.
|
||||
- Updates the `managed_tags` attribute with the acquired slots.
|
||||
- Stops leasing once the specified `max_slots` are acquired.
|
||||
- If unable to acquire the requested number of slots, logs a warning and returns the slots that were leased.
|
||||
Notes:
|
||||
- If a slot is leased but its details cannot be retrieved (i.e., `get_tag_slot` returns None),
|
||||
that slot is skipped.
|
||||
"""
|
||||
|
||||
acquired = {}
|
||||
for i in range(1, self.number_of_slots + 1):
|
||||
if self.resource_manager.lease_tag(str(i)):
|
||||
@@ -124,11 +187,41 @@ class IngestorManager():
|
||||
return acquired
|
||||
|
||||
def unsubscribe_slot(self, slot: str):
|
||||
"""
|
||||
Unsubscribes a specific slot from all associated OPC servers.
|
||||
Args:
|
||||
slot (str): The name of the slot to unsubscribe.
|
||||
Raises:
|
||||
KeyError: If the specified slot does not exist in the managed tags.
|
||||
"""
|
||||
|
||||
for server in self.managed_tags[slot].keys():
|
||||
if server in self.opc_managers:
|
||||
self.opc_managers[server].unsubscribe(slot)
|
||||
|
||||
def update_slot_config(self):
|
||||
"""
|
||||
Updates the configuration of managed slots by renewing their leases,
|
||||
fetching the latest configurations, and handling any changes or removals.
|
||||
This method performs the following steps:
|
||||
1. Renews the lease for each managed slot using the resource manager.
|
||||
2. Fetches the latest configuration for each slot.
|
||||
3. Logs and removes slots whose configurations are no longer available.
|
||||
4. Updates the configuration of slots if changes are detected.
|
||||
5. Unsubscribes and re-subscribes to slots with updated configurations.
|
||||
6. Removes slots from the managed tags if they are no longer valid.
|
||||
7. Updates the OPC servers after processing all slots.
|
||||
Side Effects:
|
||||
- Modifies the `managed_tags` dictionary to reflect the latest slot configurations.
|
||||
- Updates OPC server subscriptions based on the current state of managed slots.
|
||||
Raises:
|
||||
- None explicitly, but relies on the behavior of `resource_manager` and
|
||||
other dependencies for error handling.
|
||||
Logging:
|
||||
- Logs warnings for removed slots.
|
||||
- Logs informational messages for updated slot configurations.
|
||||
"""
|
||||
|
||||
removed_slots = []
|
||||
update = {}
|
||||
for slot, slot_config in self.managed_tags.items():
|
||||
@@ -160,54 +253,116 @@ class IngestorManager():
|
||||
self.update_opc_servers()
|
||||
|
||||
def drop_slot_leases(self, ids: List[str]) -> None:
|
||||
"""
|
||||
Releases the leases associated with the specified slot IDs.
|
||||
This method iterates through a list of slot IDs and calls the
|
||||
`drop_tag_lease` method of the `resource_manager` to release
|
||||
the lease for each ID.
|
||||
Args:
|
||||
ids (List[str]): A list of slot IDs for which the leases
|
||||
should be released.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
for id in ids:
|
||||
self.resource_manager.drop_tag_lease(id)
|
||||
|
||||
def subscribe_to_tags(self, tags: Dict) -> None: # NOSONAR
|
||||
def manage_server(self, slot: str, server: str, server_config: dict, tags: dict) -> int:
|
||||
"""
|
||||
Manages the subscription of tags to a specified OPC server and slot.
|
||||
This method ensures that the specified server and slot have an active subscription
|
||||
for the provided tags. If the server or slot is not properly configured, or if
|
||||
subscription fails, appropriate error handling is performed.
|
||||
Args:
|
||||
slot (str): The slot identifier for the subscription.
|
||||
server (str): The name of the OPC server.
|
||||
server_config (dict): Configuration dictionary for the server, which includes
|
||||
the tags to be subscribed under the key 'tags'.
|
||||
tags (dict): A dictionary of tags to be subscribed.
|
||||
Returns:
|
||||
int: Status code indicating the result of the operation:
|
||||
- 0: Subscription was successful.
|
||||
- 1: Server not found in `opc_managers`.
|
||||
- 2: Subscription creation or tag subscription failed.
|
||||
Logs:
|
||||
- Logs informational messages about the subscription process.
|
||||
- Logs errors if the server is not found, subscription creation fails, or
|
||||
tag subscription fails.
|
||||
- Logs a warning if a subscription is removed due to failure.
|
||||
Raises:
|
||||
Exception: Any unexpected exceptions during subscription creation or tag
|
||||
subscription are logged but not propagated.
|
||||
"""
|
||||
|
||||
self.logger.info(
|
||||
f"Subscribing to tags from {slot}:{server}"
|
||||
)
|
||||
tags_to_sub = server_config.get('tags')
|
||||
if server not in self.opc_managers:
|
||||
self.logger.error(
|
||||
f"Server {server} not found in opc_managers."
|
||||
)
|
||||
return 1
|
||||
if slot not in self.opc_managers[server].subscriptions:
|
||||
try:
|
||||
self.opc_managers[server].create_subscription(
|
||||
slot
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to create subscription for slot {slot}: {e}"
|
||||
)
|
||||
return 2
|
||||
try:
|
||||
self.logger.info(
|
||||
tags_to_sub
|
||||
)
|
||||
self.opc_managers[server].subscribe(
|
||||
slot, deepcopy(tags_to_sub), self.poll_interval
|
||||
)
|
||||
self.logger.info(
|
||||
tags_to_sub
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to subscribe to tags from {slot}:{server}\n{tags}: {e}"
|
||||
)
|
||||
self.logger.error(traceback.format_exc())
|
||||
self.logger.warning(
|
||||
"Removing subscription from server "
|
||||
f"{server} for slot {slot}"
|
||||
)
|
||||
self.opc_managers[server].unsubscribe(slot)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
def subscribe_to_tags(self, tags: Dict) -> None:
|
||||
"""
|
||||
Subscribes to a set of tags and manages their configurations.
|
||||
This method processes a dictionary of tags, iterating through each slot and server
|
||||
configuration. It attempts to manage the server configurations and removes any
|
||||
servers that return a specific response code.
|
||||
Args:
|
||||
tags (Dict): A dictionary containing tag configurations. The structure is
|
||||
expected to be {slot: {server: server_config}}.
|
||||
Side Effects:
|
||||
- Logs the provided tags for debugging purposes.
|
||||
- Updates the `managed_tags` attribute by removing servers that meet the
|
||||
removal criteria.
|
||||
Removal Criteria:
|
||||
- If the `manage_server` method returns a response code of 2 for a given
|
||||
slot and server, that server is removed from the `managed_tags` attribute.
|
||||
"""
|
||||
|
||||
to_remove = []
|
||||
self.logger.info(tags)
|
||||
for slot, slot_config in tags.items():
|
||||
for server, server_config in slot_config.items():
|
||||
self.logger.info(
|
||||
f"Subscribing to tags from {slot}:{server}"
|
||||
response = self.manage_server(
|
||||
slot, server, server_config, tags
|
||||
)
|
||||
tags_to_sub = server_config.get('tags')
|
||||
if server not in self.opc_managers:
|
||||
self.logger.error(
|
||||
f"Server {server} not found in opc_managers."
|
||||
)
|
||||
continue
|
||||
if slot not in self.opc_managers[server].subscriptions:
|
||||
try:
|
||||
self.opc_managers[server].create_subscription(
|
||||
slot
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to create subscription for slot {slot}: {e}"
|
||||
)
|
||||
to_remove.append([slot, server])
|
||||
continue
|
||||
try:
|
||||
self.logger.info(
|
||||
tags_to_sub
|
||||
)
|
||||
self.opc_managers[server].subscribe(
|
||||
slot, deepcopy(tags_to_sub), self.poll_interval
|
||||
)
|
||||
self.logger.info(
|
||||
tags_to_sub
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Failed to subscribe to tags from {slot}:{server}\n{tags}: {e}"
|
||||
)
|
||||
self.logger.error(traceback.format_exc())
|
||||
self.logger.warning(
|
||||
"Removing subscription from server "
|
||||
f"{server} for slot {slot}"
|
||||
)
|
||||
self.opc_managers[server].unsubscribe(slot)
|
||||
if response == 2:
|
||||
to_remove.append([slot, server])
|
||||
|
||||
for slot, server in to_remove:
|
||||
|
||||
@@ -31,21 +31,6 @@ class OpcManager():
|
||||
return f"OpcManager(name={self.name}, url={self.url}, server_uri={self.server_uri})\n" \
|
||||
f"nodes={self.nodes}, subscriptions={self.subscriptions}"
|
||||
|
||||
def initialize_from_config(self, server_config: dict, data_manager: DataManager, logger: Logger):
|
||||
"""
|
||||
Initializes the OpcManager instance using a server configuration dictionary.
|
||||
Args:
|
||||
server_config (dict): A dictionary containing server configuration details.
|
||||
data_manager (DataManager): An instance of DataManager for data handling.
|
||||
logger (Logger): Logger instance for logging information.
|
||||
"""
|
||||
self.__init__(
|
||||
server_config['name'], server_config['url'], data_manager, logger, server_config['server_uri'],
|
||||
server_config.get('cert_path'), server_config.get(
|
||||
'private_key_path'), server_config.get('server_cert_path')
|
||||
)
|
||||
self.config = server_config
|
||||
|
||||
def set_security(self):
|
||||
"""
|
||||
Configures the security settings for the OPC UA client.
|
||||
@@ -161,10 +146,22 @@ class OpcManager():
|
||||
self.subscriptions[subscription].subscribe_data_change(self.addr_nodes)
|
||||
|
||||
def unsubscribe(self, subscription: str):
|
||||
"""
|
||||
Unsubscribes from a given subscription.
|
||||
Args:
|
||||
subscription (str): The name of the subscription to unsubscribe from.
|
||||
Logs:
|
||||
- A warning if the specified subscription does not exist.
|
||||
- An info message upon successful unsubscription.
|
||||
Behavior:
|
||||
- If the subscription exists, it is deleted and removed from the
|
||||
subscriptions dictionary.
|
||||
- If the subscription does not exist, no action is taken.
|
||||
"""
|
||||
|
||||
if not self.subscriptions.get(subscription):
|
||||
self.logger.warning(
|
||||
f"Subscription {subscription} not found. Cannot unsubscribe.")
|
||||
f"Subscription '{subscription}' not found. Cannot unsubscribe.")
|
||||
return
|
||||
self.subscriptions[subscription].delete()
|
||||
del self.subscriptions[subscription]
|
||||
|
||||
Reference in New Issue
Block a user