SIENTIAPDE-1205

Refactor Ingestor and OPC Manager for Asynchronous Operations

- Updated main function to be asynchronous and integrated asyncio for better concurrency.
- Refactored Ingestor methods to support async operations, including prepare_ingestor, loop, and shutdown.
- Enhanced IngestorManager and OpcManager with async methods for improved performance and responsiveness.
- Replaced blocking calls with await statements to ensure non-blocking behavior during operations.
- Added a new run_async_main function to handle the async event loop setup.
This commit is contained in:
vitor-aignosi
2025-08-26 16:27:35 -03:00
parent ec3c738519
commit d4aa44d774
5 changed files with 111 additions and 67 deletions

View File

@@ -1,4 +1,5 @@
import os import os
import asyncio
import signal import signal
import traceback import traceback
from threading import Event from threading import Event
@@ -13,11 +14,11 @@ exit_signal = Event()
POD_ID = os.getenv("HOSTNAME", "localhost") POD_ID = os.getenv("HOSTNAME", "localhost")
def main(): async def main():
start_prometheus_server() start_prometheus_server()
ingestor = Ingestor() ingestor = Ingestor()
try: try:
ingestor.prepare_ingestor() await ingestor.prepare_ingestor()
except Exception as e: except Exception as e:
metrics.APP_ERRORS_TOTAL.labels( metrics.APP_ERRORS_TOTAL.labels(
pod_id=POD_ID).inc() # Increment errors pod_id=POD_ID).inc() # Increment errors
@@ -28,11 +29,12 @@ def main():
while not exit_signal.is_set(): while not exit_signal.is_set():
start_time = time() # Start loop timer start_time = time() # Start loop timer
try: try:
ingestor.loop() await ingestor.loop()
metrics.APP_LOOP_COUNT.labels( metrics.APP_LOOP_COUNT.labels(
pod_id=POD_ID).inc() # Increment loop counter pod_id=POD_ID).inc() # Increment loop counter
exit_signal.wait(ingestor.poll_interval) # Use asyncio.sleep instead of exit_signal.wait for better async compatibility
await asyncio.sleep(ingestor.poll_interval)
except KeyboardInterrupt: # Handle Ctrl+C gracefully except KeyboardInterrupt: # Handle Ctrl+C gracefully
print("KeyboardInterrupt received. Setting exit_signal flag.") print("KeyboardInterrupt received. Setting exit_signal flag.")
@@ -48,13 +50,13 @@ def main():
duration = time() - start_time duration = time() - start_time
metrics.APP_LOOP_DURATION.labels(pod_id=POD_ID).observe(duration) metrics.APP_LOOP_DURATION.labels(pod_id=POD_ID).observe(duration)
ingestor.shutdown() await ingestor.shutdown()
metrics.APP_UP.labels(pod_id=POD_ID).set(0) # Mark app as DOWN metrics.APP_UP.labels(pod_id=POD_ID).set(0) # Mark app as DOWN
ingestor.logger.info("Main loop exit_signaled.") ingestor.logger.info("Main loop exit_signaled.")
# Give Prometheus a chance to scrape one last time before exiting (optional) # Give Prometheus a chance to scrape one last time before exiting (optional)
sleep(5) await asyncio.sleep(5)
os._exit(0) os._exit(0)
@@ -75,8 +77,22 @@ def start_prometheus_server():
os._exit(1) os._exit(1)
def run_async_main():
"""Run the async main function with proper event loop setup"""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())
except KeyboardInterrupt:
print("KeyboardInterrupt received in main thread.")
exit_signal.set()
finally:
loop.close()
if __name__ == "__main__": if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGHUP, signal_handler)
main()
run_async_main()

View File

@@ -1,3 +1,4 @@
import asyncio
from os import getenv from os import getenv
from copy import deepcopy from copy import deepcopy
from typing import Dict, Any from typing import Dict, Any
@@ -78,14 +79,14 @@ class Ingestor:
} }
self.ingestor_manager = None self.ingestor_manager = None
def shutdown(self): async def shutdown(self):
if self.ingestor_manager: if self.ingestor_manager:
self.ingestor_manager.shutdown() await self.ingestor_manager.shutdown()
def __del__(self): def __del__(self):
self.shutdown() asyncio.run(self.shutdown())
def handle_acquired_tags(self, acquired): async def handle_acquired_tags(self, acquired):
""" """
Handles the acquired tags by subscribing to them if available. Handles the acquired tags by subscribing to them if available.
This method checks if there are any acquired tags. If no tags are acquired, This method checks if there are any acquired tags. If no tags are acquired,
@@ -103,9 +104,9 @@ class Ingestor:
else: else:
# Subscribe to acquired slots # Subscribe to acquired slots
self.ingestor_manager.update_opc_servers() self.ingestor_manager.update_opc_servers()
self.ingestor_manager.subscribe_to_tags(acquired) await self.ingestor_manager.subscribe_to_tags(acquired)
def prepare_ingestor(self): async def prepare_ingestor(self):
""" """
Prepares the ingestor by initializing the IngestorManager, declaring the ingestor as active, Prepares the ingestor by initializing the IngestorManager, declaring the ingestor as active,
acquiring slot leases, and handling the acquired tags. acquiring slot leases, and handling the acquired tags.
@@ -153,7 +154,7 @@ class Ingestor:
acquired = self.ingestor_manager.get_slot_leases() acquired = self.ingestor_manager.get_slot_leases()
self.logger.info(f"Acquired slots: {acquired}") self.logger.info(f"Acquired slots: {acquired}")
self.handle_acquired_tags(acquired) await self.handle_acquired_tags(acquired)
metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set( metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set(
len(self.ingestor_manager.managed_tags) len(self.ingestor_manager.managed_tags)
@@ -176,7 +177,7 @@ class Ingestor:
# Get slot lease # Get slot lease
self.ingestor_manager.get_slot_leases(1) self.ingestor_manager.get_slot_leases(1)
def manage_leases( async def manage_leases(
self, available_slots: int, lacking_ingestors: int, slot_diff: int self, available_slots: int, lacking_ingestors: int, slot_diff: int
): ):
""" """
@@ -214,7 +215,7 @@ class Ingestor:
self.ingestor_manager.drop_slot_leases(overleases) self.ingestor_manager.drop_slot_leases(overleases)
for lease in overleases: for lease in overleases:
self.ingestor_manager.unsubscribe_slot(lease) await self.ingestor_manager.unsubscribe_slot(lease)
self.ingestor_manager.managed_tags.pop(lease) self.ingestor_manager.managed_tags.pop(lease)
# Update metric after removal # Update metric after removal
@@ -222,7 +223,7 @@ class Ingestor:
len(self.ingestor_manager.managed_tags) len(self.ingestor_manager.managed_tags)
) )
def update_ingestor_manager(self, old_managed_tags: Dict[str, Any]): async def update_ingestor_manager(self, old_managed_tags: Dict[str, Any]):
""" """
Updates the ingestor manager with the new managed tags. Updates the ingestor manager with the new managed tags.
Args: Args:
@@ -232,7 +233,7 @@ class Ingestor:
self.logger.debug( self.logger.debug(
f"Current managed tags: {self.ingestor_manager.managed_tags}") f"Current managed tags: {self.ingestor_manager.managed_tags}")
self.ingestor_manager.update_opc_servers() await self.ingestor_manager.update_opc_servers()
new_managed_tags = deepcopy(self.ingestor_manager.managed_tags) new_managed_tags = deepcopy(self.ingestor_manager.managed_tags)
self.logger.debug( self.logger.debug(
@@ -249,25 +250,25 @@ class Ingestor:
for slot, config in new_managed_tags.items(): for slot, config in new_managed_tags.items():
if slot not in old_managed_tags: if slot not in old_managed_tags:
self.logger.info(f"Subscribing to new slot {slot}") self.logger.info(f"Subscribing to new slot {slot}")
self.ingestor_manager.subscribe_to_tags({slot: config}) await self.ingestor_manager.subscribe_to_tags({slot: config})
continue continue
if config != old_managed_tags[slot]: if config != old_managed_tags[slot]:
self.logger.info(f"Resubscribing to slot {slot}") self.logger.info(f"Resubscribing to slot {slot}")
self.ingestor_manager.unsubscribe_slot(slot) await self.ingestor_manager.unsubscribe_slot(slot)
self.ingestor_manager.subscribe_to_tags({slot: config}) await self.ingestor_manager.subscribe_to_tags({slot: config})
for slot in old_managed_tags.keys(): for slot in old_managed_tags.keys():
if slot not in new_managed_tags: if slot not in new_managed_tags:
self.logger.info(f"Unsubscribing from slot {slot}") self.logger.info(f"Unsubscribing from slot {slot}")
self.ingestor_manager.unsubscribe_slot(slot) await self.ingestor_manager.unsubscribe_slot(slot)
# Ensure the gauge is updated after any potential changes here # Ensure the gauge is updated after any potential changes here
metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set( metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set(
len(self.ingestor_manager.managed_tags) len(self.ingestor_manager.managed_tags)
) )
def loop(self): async def loop(self):
""" """
Executes the main loop for managing ingestors and slots. Executes the main loop for managing ingestors and slots.
This method performs the following tasks: This method performs the following tasks:
@@ -310,7 +311,7 @@ class Ingestor:
slot_diff = len(self.ingestor_manager.managed_tags) - 1 slot_diff = len(self.ingestor_manager.managed_tags) - 1
self.logger.info("Managing leases...") self.logger.info("Managing leases...")
self.manage_leases(available_slots, lacking_ingestors, slot_diff) await self.manage_leases(available_slots, lacking_ingestors, slot_diff)
# Update managed slots gauge # Update managed slots gauge
metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set( metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set(
@@ -337,4 +338,4 @@ class Ingestor:
self.ingestor_manager.check_opc_servers_integrity() self.ingestor_manager.check_opc_servers_integrity()
self.logger.info("Updating managed tags...") self.logger.info("Updating managed tags...")
self.update_ingestor_manager(current_managed_tags) await self.update_ingestor_manager(current_managed_tags)

View File

@@ -1,3 +1,4 @@
import asyncio
import traceback import traceback
from typing import Dict, List from typing import Dict, List
from copy import deepcopy from copy import deepcopy
@@ -57,7 +58,7 @@ class IngestorManager(BaseActivity):
notification_handler=notification_handler, notification_handler=notification_handler,
set_error_counter=True) set_error_counter=True)
def initialize_opc_from_config(self, server_config: dict) -> OpcManager | None: async def initialize_opc_from_config(self, server_config: dict) -> OpcManager | None:
""" """
Initializes an OPC Manager instance using the provided server configuration. Initializes an OPC Manager instance using the provided server configuration.
Args: Args:
@@ -93,7 +94,7 @@ class IngestorManager(BaseActivity):
) )
manager.config = server_config manager.config = server_config
manager.connect() await manager.connect()
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()
@@ -111,15 +112,15 @@ class IngestorManager(BaseActivity):
return manager return manager
def shutdown(self): async def shutdown(self):
for _server_name, server in self.opc_managers.items(): for _server_name, server in self.opc_managers.items():
server.disconnect() await server.disconnect()
self.data_manager.shutdown() self.data_manager.shutdown()
def __del__(self): def __del__(self):
self.shutdown() asyncio.run(self.shutdown())
def update_opc_servers(self): async def update_opc_servers(self):
""" """
Updates the OPC (OLE for Process Control) server connections managed by the ingestor. 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 This method ensures that the OPC servers defined in `self.managed_tags` are properly
@@ -156,7 +157,7 @@ class IngestorManager(BaseActivity):
self.logger.info( self.logger.info(
f"Initializing OPC manager for server {server}" f"Initializing OPC manager for server {server}"
) )
server_instance = self.initialize_opc_from_config( server_instance = await self.initialize_opc_from_config(
server_config server_config
) )
@@ -166,7 +167,7 @@ class IngestorManager(BaseActivity):
) )
server_instance.disconnect() server_instance.disconnect()
del self.opc_managers[server] del self.opc_managers[server]
server_instance = self.initialize_opc_from_config( server_instance = await self.initialize_opc_from_config(
server_config server_config
) )
else: else:
@@ -191,7 +192,7 @@ class IngestorManager(BaseActivity):
f"Server {server} not found in managed tags. " f"Server {server} not found in managed tags. "
f"Desconnecting from server." f"Desconnecting from server."
) )
self.opc_managers[server].disconnect() await self.opc_managers[server].disconnect()
self.opc_managers.pop(server, None) self.opc_managers.pop(server, None)
metrics.OPC_MANAGERS_ACTIVE.labels( metrics.OPC_MANAGERS_ACTIVE.labels(
@@ -312,7 +313,7 @@ class IngestorManager(BaseActivity):
pod_id=self.pod_id).set(len(self.managed_tags)) pod_id=self.pod_id).set(len(self.managed_tags))
return acquired return acquired
def unsubscribe_slot(self, slot: str): async def unsubscribe_slot(self, slot: str):
""" """
Unsubscribes a specific slot from all associated OPC servers. Unsubscribes a specific slot from all associated OPC servers.
Args: Args:
@@ -323,7 +324,7 @@ class IngestorManager(BaseActivity):
for server in self.managed_tags[slot].keys(): for server in self.managed_tags[slot].keys():
if server in self.opc_managers: if server in self.opc_managers:
self.opc_managers[server].unsubscribe(slot) await self.opc_managers[server].unsubscribe(slot)
def update_slot_config(self): def update_slot_config(self):
""" """
@@ -378,7 +379,7 @@ class IngestorManager(BaseActivity):
self.resource_manager.drop_tag_lease(lease_id) self.resource_manager.drop_tag_lease(lease_id)
metrics.SLOTS_RELEASED.labels(pod_id=self.pod_id).inc() metrics.SLOTS_RELEASED.labels(pod_id=self.pod_id).inc()
def manage_server(self, slot: str, server: str, server_config: dict, tags: dict) -> int: async 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. 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 This method ensures that the specified server and slot have an active subscription
@@ -416,7 +417,7 @@ class IngestorManager(BaseActivity):
return 1 return 1
if slot not in self.opc_managers[server].subscriptions: if slot not in self.opc_managers[server].subscriptions:
try: try:
self.opc_managers[server].create_subscription( await self.opc_managers[server].create_subscription(
slot slot
) )
except Exception as e: except Exception as e:
@@ -428,7 +429,7 @@ class IngestorManager(BaseActivity):
self.logger.info( self.logger.info(
tags_to_sub tags_to_sub
) )
self.opc_managers[server].subscribe( await self.opc_managers[server].subscribe(
slot, deepcopy(tags_to_sub), self.poll_interval slot, deepcopy(tags_to_sub), self.poll_interval
) )
self.logger.info( self.logger.info(
@@ -452,11 +453,11 @@ class IngestorManager(BaseActivity):
"Removing subscription from server " "Removing subscription from server "
f"{server} for slot {slot}" f"{server} for slot {slot}"
) )
self.opc_managers[server].unsubscribe(slot) await self.opc_managers[server].unsubscribe(slot)
return 2 return 2
return 0 return 0
def subscribe_to_tags(self, tags: Dict) -> None: async def subscribe_to_tags(self, tags: Dict) -> None:
""" """
Subscribes to a set of tags and manages their configurations. Subscribes to a set of tags and manages their configurations.
This method processes a dictionary of tags, iterating through each slot and server This method processes a dictionary of tags, iterating through each slot and server
@@ -478,7 +479,7 @@ class IngestorManager(BaseActivity):
self.logger.info(tags) self.logger.info(tags)
for slot, slot_config in tags.items(): for slot, slot_config in tags.items():
for server, server_config in slot_config.items(): for server, server_config in slot_config.items():
response = self.manage_server( response = await self.manage_server(
slot, server, server_config, tags slot, server, server_config, tags
) )
if response == 2: if response == 2:

View File

@@ -1,7 +1,8 @@
import json import json
import asyncio
from pathlib import Path from pathlib import Path
from asyncua.crypto.security_policies import SecurityPolicyBasic256 from asyncua.crypto.security_policies import SecurityPolicyBasic256
from asyncua.sync import Client from asyncua import Client
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.temporal.activities.base import BaseActivity from sientia_do.temporal.activities.base import BaseActivity
@@ -42,7 +43,18 @@ class OpcManager(BaseActivity):
return f"OpcManager(name={self.name}, url={self.url}, server_uri={self.server_uri})\n" \ return f"OpcManager(name={self.name}, url={self.url}, server_uri={self.server_uri})\n" \
f"nodes={self.nodes}, subscriptions={self.subscriptions}" f"nodes={self.nodes}, subscriptions={self.subscriptions}"
def set_security(self): async def shutdown(self):
"""Comprehensive cleanup method"""
try:
await self.disconnect()
except Exception as e:
self.logger.error(f"Error during cleanup: {e}")
def __del__(self):
asyncio.run(self.shutdown())
async def set_security(self):
""" """
Configures the security settings for the OPC UA client. Configures the security settings for the OPC UA client.
This method sets up the security policy, certificates, and timeouts This method sets up the security policy, certificates, and timeouts
@@ -70,18 +82,18 @@ class OpcManager(BaseActivity):
server_cert = Path( server_cert = Path(
self.server_cert_path) if self.server_cert_path else None self.server_cert_path) if self.server_cert_path else None
self.client.application_uri = self.server_uri await self.client.set_application_uri(self.server_uri)
self.logger.info('Setting security...') self.logger.info('Setting security...')
self.client.set_security( await self.client.set_security(
SecurityPolicyBasic256, SecurityPolicyBasic256,
certificate=str(cert), certificate=str(cert),
private_key=str(private_key), private_key=str(private_key),
server_certificate=str(server_cert) server_certificate=str(server_cert)
) )
self.client.secure_channel_timeout = 10000000 await self.client.set_secure_channel_timeout(10000000)
self.client.session_timeout = 10000000 await self.client.set_session_timeout(10000000)
def connect(self): async def connect(self):
""" """
Establishes a connection to the OPC server. Establishes a connection to the OPC server.
This method initializes the OPC client using the provided URL and This method initializes the OPC client using the provided URL and
@@ -96,9 +108,9 @@ class OpcManager(BaseActivity):
try: try:
self.client = Client(self.url) self.client = Client(self.url)
if self.cert_path: if self.cert_path:
self.set_security() await self.set_security()
self.logger.info(f'Starting connection to {self.name}...') self.logger.info(f'Starting connection to {self.name}...')
self.client.connect() await self.client.connect()
metrics.OPC_CONNECTION_STATUS.labels( metrics.OPC_CONNECTION_STATUS.labels(
pod_id=self.pod_id, server_name=self.name, server_url=self.url).set(1) pod_id=self.pod_id, server_name=self.name, server_url=self.url).set(1)
self.logger.info(f'Connection to {self.name} successful.') self.logger.info(f'Connection to {self.name} successful.')
@@ -110,7 +122,7 @@ class OpcManager(BaseActivity):
self.logger.error(f"Failed to connect to {self.name}: {e}") self.logger.error(f"Failed to connect to {self.name}: {e}")
raise raise
def create_subscription(self, name: str, period: int = 500): async def create_subscription(self, name: str, period: int = 500):
""" """
Creates a subscription with the specified monitoring period. Creates a subscription with the specified monitoring period.
This method establishes a subscription to monitor data changes or events This method establishes a subscription to monitor data changes or events
@@ -129,7 +141,7 @@ class OpcManager(BaseActivity):
raise ValueError("Client not connected. Call connect first.") raise ValueError("Client not connected. Call connect first.")
try: try:
p = period if period is not None else 500 p = period if period is not None else 500
self.subscriptions[name] = self.client.create_subscription(p, self) self.subscriptions[name] = await self.client.create_subscription(p, self)
self.logger.info(f'Subscription {name} created on {self.name}.') self.logger.info(f'Subscription {name} created on {self.name}.')
metrics.OPC_SUBSCRIPTIONS_CREATED.labels( metrics.OPC_SUBSCRIPTIONS_CREATED.labels(
pod_id=self.pod_id, server_name=self.name, slot_name=name).inc() pod_id=self.pod_id, server_name=self.name, slot_name=name).inc()
@@ -138,7 +150,7 @@ class OpcManager(BaseActivity):
f"Failed to create subscription {name} on {self.name}: {e}") f"Failed to create subscription {name} on {self.name}: {e}")
raise raise
def subscribe(self, subscription: str, nodes: dict, collect_period: int): async def subscribe(self, subscription: str, nodes: dict, collect_period: int):
""" """
Subscribes to a set of OPC UA nodes for data change notifications. Subscribes to a set of OPC UA nodes for data change notifications.
This method adds the specified nodes to the subscription and configures This method adds the specified nodes to the subscription and configures
@@ -161,8 +173,7 @@ class OpcManager(BaseActivity):
self.logger.info(f"Subscribing to {subscription} on {self.name}...") self.logger.info(f"Subscribing to {subscription} on {self.name}...")
self.logger.info(f"Subscribing to nodes: {nodes}") self.logger.info(f"Subscribing to nodes: {nodes}")
addr_nodes = [self.client.get_node( addr_nodes = [self.client.get_node(n) for n in nodes]
n) for n in nodes]
self.logger.debug(f"Addr nodes: {addr_nodes}") self.logger.debug(f"Addr nodes: {addr_nodes}")
self.nodes.update(nodes) self.nodes.update(nodes)
self.logger.debug(f"Nodes: {self.nodes}") self.logger.debug(f"Nodes: {self.nodes}")
@@ -176,9 +187,9 @@ class OpcManager(BaseActivity):
'cycle_count': 0 'cycle_count': 0
} }
self.subscriptions[subscription].subscribe_data_change(addr_nodes) await self.subscriptions[subscription].subscribe_data_change(addr_nodes)
def unsubscribe(self, subscription: str): async def unsubscribe(self, subscription: str):
""" """
Unsubscribes from a given subscription. Unsubscribes from a given subscription.
Args: Args:
@@ -196,14 +207,11 @@ class OpcManager(BaseActivity):
self.logger.warning( self.logger.warning(
f"Subscription '{subscription}' not found. Cannot unsubscribe.") f"Subscription '{subscription}' not found. Cannot unsubscribe.")
return return
self.subscriptions[subscription].delete() await self.subscriptions[subscription].delete()
del self.subscriptions[subscription] del self.subscriptions[subscription]
self.logger.info(f"Unsubscribed from {subscription}.") self.logger.info(f"Unsubscribed from {subscription}.")
def __del__(self): async def disconnect(self):
self.disconnect()
def disconnect(self):
""" """
Disconnects from the OPC UA server. Disconnects from the OPC UA server.
This method handles the disconnection process by deleting the subscription This method handles the disconnection process by deleting the subscription
@@ -219,14 +227,14 @@ class OpcManager(BaseActivity):
self.logger.warning("Client already disconnected.") self.logger.warning("Client already disconnected.")
return return
try: try:
_a = [self.subscriptions[sub].delete() for sub in self.subscriptions:
for sub in self.subscriptions] await self.subscriptions[sub].delete()
self.logger.warning("Deleted all subscriptions.") self.logger.warning("Deleted all subscriptions.")
except Exception as sub_error: except Exception as sub_error:
self.logger.error(f"Failed to clean up subscription: {sub_error}") self.logger.error(f"Failed to clean up subscription: {sub_error}")
try: try:
self.client.disconnect() await self.client.disconnect()
except Exception as conn_error: except Exception as conn_error:
self.logger.error( self.logger.error(
f"Failed to disconnect from OPC UA server: {conn_error}") f"Failed to disconnect from OPC UA server: {conn_error}")
@@ -239,7 +247,7 @@ class OpcManager(BaseActivity):
pod_id=self.pod_id, server_name=self.name).set(0) pod_id=self.pod_id, server_name=self.name).set(0)
self.logger.warning("Disconnected from OPC UA server.") self.logger.warning("Disconnected from OPC UA server.")
def datachange_notification(self, node, _val, data): async def datachange_notification(self, node, _val, data):
""" """
Handles data change notifications for monitored OPC UA nodes. Handles data change notifications for monitored OPC UA nodes.
This method is triggered when a monitored node's value changes. It processes This method is triggered when a monitored node's value changes. It processes

18
run_local.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Exit on any error
set -e
echo "Activating virtual environment..."
source ./venv/bin/activate
echo "Loading environment variables from .env..."
if [ -f .env ]; then
export $(cat .env | grep -v '^#' | xargs)
echo "Environment variables loaded from .env"
else
echo "Warning: .env file not found. Continuing without environment variables."
fi
echo "Starting ingestor application..."
python -m ingestor.app