Merge pull request #20 from Aignosi/SIENTIAPDE-1318-adicionar-pipeline-de-qualidade

Chore: Update dependencies, improve CI, and enhance code formatting
This commit is contained in:
Matheus Demoner
2025-10-20 08:37:51 -03:00
committed by GitHub
22 changed files with 1644 additions and 1432 deletions

View File

@@ -1,5 +1,6 @@
name: Quality gate name: Quality gate
on: on:
push: push:
branches: branches:
@@ -19,6 +20,30 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false
- name: Generate App Token
id: generate-app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: 'Aignosi'
repositories: 'sientia-dataops-library,sientia-mlops-library'
- name: Prepare requirements.txt
id: prepare-requirements
run: |
sed -e "s|git+ssh://git@github.com/|git+https://github.com/|g" \
-e "s|git@github.com:|git+https://github.com/|g" \
requirements.txt > requirements_prepared.txt
echo "PROCESSED_REQUIREMENTS_FILE=requirements_prepared.txt" >> $GITHUB_OUTPUT
- name: Configure Git to use App Token
env:
GH_APP_TOKEN: ${{ steps.generate-app-token.outputs.token }}
run: |
git config --global url."https://oauth2:${GH_APP_TOKEN}@github.com/".insteadOf "https://github.com/"
- name: 🔧 Setup Python - name: 🔧 Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
@@ -29,25 +54,42 @@ jobs:
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles(steps.prepare-requirements.outputs.PROCESSED_REQUIREMENTS_FILE) }}
restore-keys: | restore-keys: |
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Configure SSH - name: 📦 Install Development Dependencies
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Add github.com to known_hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
- name: 📦 Install Dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements-dev.txt
pip install pytest pytest-cov pytest-asyncio
- name: 📦 Install Runtime Dependencies
run: |
pip install -r ${{ steps.prepare-requirements.outputs.PROCESSED_REQUIREMENTS_FILE }}
- name: 📝 Code Formatting Check (Ruff)
run: |
echo "Checking code formatting..."
ruff format --check ingestor/ tests/
continue-on-error: false
- name: 🔎 Code Linting (Ruff)
run: |
echo "Running linting checks..."
ruff check ingestor/ tests/
continue-on-error: false
- name: 🏷️ Type Checking (mypy)
run: |
echo "Running type checks..."
mypy ingestor/
continue-on-error: true
- name: 🔒 Security Analysis (Bandit)
run: |
echo "Running security analysis..."
bandit -r ingestor/ -ll -q
continue-on-error: true
- name: 🧪 Run Tests with Pytest - name: 🧪 Run Tests with Pytest
run: | run: |

4
.gitignore vendored
View File

@@ -178,3 +178,7 @@ cython_debug/
.vscode/ .vscode/
git_log git_log
.git/
.ruff_cache/
.mypy_cache/

View File

112
encrypt.py Normal file
View File

@@ -0,0 +1,112 @@
import os
import argparse
from pathspec import PathSpec
import yaml
'''
Usage:
python .\encrypt.py path_to_dir output_file --ignore ignore_file --chunk-size 100000
'''
def load_ignore_patterns(ignore_file, include_library):
# Ensure the .gitignore file exists
if not os.path.exists(ignore_file):
raise FileNotFoundError(f"Ignore file not found at {ignore_file}")
# Load and parse the .gitignore patterns
with open(ignore_file, 'r') as file:
patterns = file.readlines()
if not include_library:
patterns.append('**/deploy/library/')
spec = PathSpec.from_lines('gitwildmatch', patterns)
return spec
def is_ignored(file_path, spec):
"""Check if a file should be ignored based on the ignore patterns."""
return spec.match_file(file_path) if spec else False
def encode_file_tree_to_yaml(directory, ignore_file, include_library):
"""Encode the file tree into a single YAML file."""
ignore_patterns = load_ignore_patterns(
ignore_file, include_library) if ignore_file else None
file_tree = {}
for root, dirs, files in os.walk(directory):
# Skip ignored directories
dirs[:] = [d for d in dirs if not is_ignored(
os.path.join(root, d), ignore_patterns)]
for file in files:
file_path = os.path.join(root, file)
# Skip ignored files
if is_ignored(file_path, ignore_patterns):
continue
# Read file content
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f"Error reading file {file_path}: {e}")
raise
# Create nested dictionary structure
path_parts = os.path.relpath(file_path, directory).split(os.sep)
current_level = file_tree
# all except the last part (the file name)
for part in path_parts[:-1]:
current_level = current_level.setdefault(part, {})
# Add the file and its content
current_level[path_parts[-1]] = content
return yaml.dump(file_tree, default_flow_style=False)
def chunk_and_write_file_tree_to_yaml(yaml_content, output_file, chunk_size=None):
"""Chunk the YAML content and write it to the output file."""
chunks = [yaml_content] if chunk_size is None else [
yaml_content[i:i + chunk_size] for i in range(0, len(yaml_content), chunk_size)]
for i, chunk in enumerate(chunks):
chunk_file = f"{output_file}_{i}.yaml"
# Write the file tree to the output YAML file
with open(chunk_file, 'w', encoding='utf-8') as yaml_file:
yaml_file.write(chunk)
def main():
parser = argparse.ArgumentParser(
description="Encrypts file tree to yaml file")
parser.add_argument("input_directory", help="Directory to encode")
parser.add_argument("output_yaml_file", help="Output YAML file")
parser.add_argument("--ignore", default=None,
help="Path to the ignore file")
parser.add_argument("--chunk-size", type=int, default=None,
help="Chunk size for the output YAML file")
parser.add_argument("--library", type=bool, default=False,
help="Incude the library in the output YAML file")
# Parse arguments
args = parser.parse_args()
# Example usage
directory_to_encode = args.input_directory
ignore_file_path = args.ignore
output_yaml_file = args.output_yaml_file
include_library = args.library
content = encode_file_tree_to_yaml(
directory_to_encode, ignore_file_path, include_library)
chunk_and_write_file_tree_to_yaml(
content, output_yaml_file, args.chunk_size)
if __name__ == "__main__":
main()

View File

@@ -1,9 +1,9 @@
import os
import asyncio import asyncio
import os
import signal import signal
import traceback import traceback
from threading import Event from threading import Event
from time import sleep, time from time import time
from prometheus_client import start_http_server from prometheus_client import start_http_server
@@ -11,10 +11,10 @@ import ingestor.metrics as metrics
from ingestor.ingestor import Ingestor from ingestor.ingestor import Ingestor
exit_signal = Event() exit_signal = Event()
POD_ID = os.getenv("HOSTNAME", "localhost") POD_ID = os.getenv('HOSTNAME', 'localhost')
async def main(): async def main(): # NOSONAR
""" """
Main asynchronous function that orchestrates the OPC Ingestor application. Main asynchronous function that orchestrates the OPC Ingestor application.
@@ -43,30 +43,27 @@ async def main():
try: try:
await 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 ingestor.logger.error(f'Failed to prepare ingestor: {e}')
ingestor.logger.error(f"Failed to prepare ingestor: {e}")
exit_signal.set() exit_signal.set()
ingestor.logger.info("Ingestor prepared. Starting main loop.") ingestor.logger.info('Ingestor prepared. Starting main loop.')
while not exit_signal.is_set(): while not exit_signal.is_set(): # NOSONAR
start_time = time() # Start loop timer start_time = time() # Start loop timer
try: try:
await 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
# Use asyncio.sleep instead of exit_signal.wait for better async compatibility # Use asyncio.sleep instead of exit_signal.wait for better async compatibility
await asyncio.sleep(ingestor.poll_interval) await asyncio.sleep(ingestor.poll_interval) # NOSONAR
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.')
exit_signal.set() exit_signal.set()
except Exception: except Exception:
print("Exception in main loop. Setting exit_signal flag.") print('Exception in main loop. Setting exit_signal flag.')
traceback.print_exc() traceback.print_exc()
metrics.APP_ERRORS_TOTAL.labels( metrics.APP_ERRORS_TOTAL.labels(pod_id=POD_ID).inc() # Increment errors
pod_id=POD_ID).inc() # Increment errors
exit_signal.set() exit_signal.set()
finally: finally:
# Record loop duration # Record loop duration
@@ -76,7 +73,7 @@ async def main():
await 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)
await asyncio.sleep(5) await asyncio.sleep(5)
@@ -96,7 +93,7 @@ def signal_handler(_signum, _frame):
_signum: The signal number received _signum: The signal number received
_frame: The current stack frame (unused) _frame: The current stack frame (unused)
""" """
print(f"Received signal {_signum}. Setting exit_signal flag.") print(f'Received signal {_signum}. Setting exit_signal flag.')
exit_signal.set() exit_signal.set()
@@ -115,12 +112,12 @@ def start_prometheus_server():
Exception: If the server fails to start, the application will exit Exception: If the server fails to start, the application will exit
""" """
try: try:
port = int(os.getenv("HTTP_METRICS_PORT", 9090)) port = int(os.getenv('HTTP_METRICS_PORT', 9090))
start_http_server(port) start_http_server(port)
print(f"Prometheus server started on port {port}.") print(f'Prometheus server started on port {port}.')
metrics.APP_UP.labels(pod_id=POD_ID).set(1) # Mark app as UP metrics.APP_UP.labels(pod_id=POD_ID).set(1) # Mark app as UP
except Exception as e: except Exception as e:
print(f"Failed to start Prometheus server: {e}") print(f'Failed to start Prometheus server: {e}')
os._exit(1) os._exit(1)
@@ -139,13 +136,13 @@ def run_async_main():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop.run_until_complete(main()) loop.run_until_complete(main())
except KeyboardInterrupt: except KeyboardInterrupt:
print("KeyboardInterrupt received in main thread.") print('KeyboardInterrupt received in main thread.')
exit_signal.set() exit_signal.set()
finally: finally:
loop.close() 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)

View File

@@ -1,14 +1,12 @@
import asyncio
from os import getenv
from copy import deepcopy from copy import deepcopy
from typing import Dict, Any from os import getenv
from typing import Any
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.observability.logger import get_logger from sientia_do.observability.logger import get_logger
from ingestor.managers.ingestor_manager import IngestorManager
import ingestor.metrics as metrics import ingestor.metrics as metrics
from ingestor.managers.ingestor_manager import IngestorManager
class Ingestor: class Ingestor:
@@ -73,36 +71,31 @@ class Ingestor:
- Metrics collection and monitoring - Metrics collection and monitoring
""" """
kafka_servers = getenv("KAFKA_SERVERS", "localhost:9092") kafka_servers = getenv('KAFKA_SERVERS', 'localhost:9092')
export_to_kafka = getenv("EXPORT_TO_KAFKA", "false") export_to_kafka: bool = getenv('EXPORT_TO_KAFKA', 'false') == 'true'
if export_to_kafka and export_to_kafka == "true":
export_to_kafka = True
else:
export_to_kafka = False
self.export_to_kafka = export_to_kafka self.export_to_kafka = export_to_kafka
self.redis_host = getenv("REDIS_HOST", "localhost") self.redis_host = getenv('REDIS_HOST', 'localhost')
self.redis_port = int(getenv("REDIS_PORT", "6379")) self.redis_port = int(getenv('REDIS_PORT', '6379'))
self.redis_username = getenv("REDIS_USERNAME", None) self.redis_username = getenv('REDIS_USERNAME', None)
self.redis_password = getenv("REDIS_PASSWORD", None) self.redis_password = getenv('REDIS_PASSWORD', None)
self.lease_ttl = int(getenv("LEASE_TTL", "10")) self.lease_ttl = int(getenv('LEASE_TTL', '10'))
self.heartbeat_ttl = int(getenv("HEARTBEAT_TTL", "20")) self.heartbeat_ttl = int(getenv('HEARTBEAT_TTL', '20'))
self.pod_id = getenv("HOSTNAME", "localhost") self.pod_id = getenv('HOSTNAME', 'localhost')
self.poll_interval = int(getenv("POLL_INTERVAL", "5")) self.poll_interval = int(getenv('POLL_INTERVAL', '5'))
mongo_url = getenv("MONGODB_URL", "localhost:27017") mongo_url = getenv('MONGODB_URL', 'localhost:27017')
mongo_username = getenv("MONGODB_USERNAME", "sientia") mongo_username = getenv('MONGODB_USERNAME', 'sientia')
mongo_password = getenv("MONGODB_PASSWORD", "sientia") mongo_password = getenv('MONGODB_PASSWORD', 'sientia')
self.mongo_database = getenv("MONGODB_DATABASE", "sientia") self.mongo_database = getenv('MONGODB_DATABASE', 'sientia')
self.mongo_connection_string = f"mongodb://{mongo_username}:{mongo_password}@{mongo_url}" self.mongo_connection_string = f'mongodb://{mongo_username}:{mongo_password}@{mongo_url}'
self.kafka_servers = kafka_servers.split(",") self.kafka_servers = kafka_servers.split(',')
self.logger = get_logger(__name__) self.logger = get_logger(__name__)
self.notification_handler = NotificationHandler( self.notification_handler = NotificationHandler(
connection_string=self.mongo_connection_string, connection_string=self.mongo_connection_string,
database=self.mongo_database, database=self.mongo_database,
logger=self.logger, logger=self.logger,
project_name="opc_ingestor" project_name='opc_ingestor',
) )
self.metadata = { self.metadata = {
@@ -112,7 +105,7 @@ class Ingestor:
'schema_name': 'opc_ingestor', 'schema_name': 'opc_ingestor',
'pod_id': self.pod_id, 'pod_id': self.pod_id,
} }
self.ingestor_manager = None self.ingestor_manager: IngestorManager | None = None
async def shutdown(self): async def shutdown(self):
""" """
@@ -148,10 +141,11 @@ class Ingestor:
""" """
if not acquired: if not acquired:
self.logger.warning("No slots available") self.logger.warning('No slots available')
else: else:
# Subscribe to acquired slots # Subscribe to acquired slots
if self.ingestor_manager:
self.ingestor_manager.update_opc_servers() self.ingestor_manager.update_opc_servers()
await self.ingestor_manager.subscribe_to_tags(acquired) await self.ingestor_manager.subscribe_to_tags(acquired)
@@ -175,7 +169,7 @@ class Ingestor:
""" """
self.ingestor_manager = IngestorManager( self.ingestor_manager = IngestorManager(
kafka_servers=self.kafka_servers, kafka_servers=','.join(self.kafka_servers),
redis_data={ redis_data={
'host': self.redis_host, 'host': self.redis_host,
'port': self.redis_port, 'port': self.redis_port,
@@ -192,13 +186,14 @@ class Ingestor:
notification_handler=self.notification_handler, notification_handler=self.notification_handler,
export_to_kafka=self.export_to_kafka, export_to_kafka=self.export_to_kafka,
) )
assert self.ingestor_manager is not None
# Declare ingestor active # Declare ingestor active
self.ingestor_manager.declare_active() self.ingestor_manager.declare_active()
# Get slot lease # Get slot lease
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}')
await self.handle_acquired_tags(acquired) await self.handle_acquired_tags(acquired)
@@ -223,15 +218,13 @@ class Ingestor:
- Requests a single slot lease to begin processing - Requests a single slot lease to begin processing
""" """
if not self.ingestor_manager.managed_tags and number_of_slots > 0: if self.ingestor_manager and not self.ingestor_manager.managed_tags and number_of_slots > 0:
# This ingestor is active and has no slots, so we need to try to # This ingestor is active and has no slots, so we need to try to
# Get slot lease # Get slot lease
self.ingestor_manager.get_slot_leases(1) self.ingestor_manager.get_slot_leases(1)
async 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
):
""" """
Manages the allocation and deallocation of slot leases for ingestors. Manages the allocation and deallocation of slot leases for ingestors.
@@ -254,17 +247,18 @@ class Ingestor:
- Logs the number of available slots when attempting to acquire leases. - Logs the number of available slots when attempting to acquire leases.
- Logs the number of extra slots when releasing leases. - Logs the number of extra slots when releasing leases.
""" """
if not self.ingestor_manager:
return
if available_slots > 0 and lacking_ingestors > 0: if available_slots > 0 and lacking_ingestors > 0:
# Some ingestors are inactive, so there are "available_slots" slots available # Some ingestors are inactive, so there are "available_slots" slots available
self.logger.info(f"Slots available: {available_slots}") self.logger.info(f'Slots available: {available_slots}')
# Get slot lease # Get slot lease
self.ingestor_manager.get_slot_leases(available_slots) self.ingestor_manager.get_slot_leases(available_slots)
elif lacking_ingestors <= 0 and slot_diff > 0: elif lacking_ingestors <= 0 and slot_diff > 0:
self.logger.info(f'Extra slots available: {slot_diff}')
self.logger.info(f"Extra slots available: {slot_diff}")
# There's enough slots for all ingestors, but this ingestor has more than one slot # There's enough slots for all ingestors, but this ingestor has more than one slot
# So we need to drop the extra leases # So we need to drop the extra leases
@@ -281,7 +275,7 @@ class Ingestor:
len(self.ingestor_manager.managed_tags) len(self.ingestor_manager.managed_tags)
) )
async 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 new managed tags and handles configuration changes. Updates the ingestor manager with new managed tags and handles configuration changes.
@@ -302,37 +296,42 @@ class Ingestor:
- Updates metrics to reflect current state - Updates metrics to reflect current state
""" """
self.logger.debug( if not self.ingestor_manager:
f"Current managed tags: {self.ingestor_manager.managed_tags}") return
self.logger.debug(f'Current managed tags: {self.ingestor_manager.managed_tags}')
await 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(
f"Comparing new managed tags {new_managed_tags} with old managed tags {old_managed_tags}" f'Comparing new managed tags {new_managed_tags} with old managed tags {old_managed_tags}'
) )
keys = set(new_managed_tags) | set(old_managed_tags) keys = set(new_managed_tags) | set(old_managed_tags)
changes = {k: (new_managed_tags.get(k), old_managed_tags.get(k)) changes = {
for k in keys if new_managed_tags.get(k) != old_managed_tags.get(k)} k: (new_managed_tags.get(k), old_managed_tags.get(k))
for k in keys
if new_managed_tags.get(k) != old_managed_tags.get(k)
}
self.logger.debug(f"Changes: {changes}") self.logger.debug(f'Changes: {changes}')
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}')
await 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}')
await self.ingestor_manager.unsubscribe_slot(slot) await self.ingestor_manager.unsubscribe_slot(slot)
await 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}')
await 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
@@ -364,9 +363,12 @@ class Ingestor:
and OPC servers. and OPC servers.
""" """
if not self.ingestor_manager:
return
self.ingestor_manager.declare_active() self.ingestor_manager.declare_active()
self.logger.info("Polling for slot updates...") self.logger.info('Polling for slot updates...')
# Get active ingestors # Get active ingestors
current_managed_tags = deepcopy(self.ingestor_manager.managed_tags) current_managed_tags = deepcopy(self.ingestor_manager.managed_tags)
@@ -380,14 +382,14 @@ class Ingestor:
metrics.ACTIVE_INGESTORS.set(number_of_ingestors) metrics.ACTIVE_INGESTORS.set(number_of_ingestors)
# Handle no slots # Handle no slots
self.logger.info("Managing no slots...") self.logger.info('Managing no slots...')
self.manage_no_slots(number_of_slots) self.manage_no_slots(number_of_slots)
available_slots = number_of_slots - number_of_leases available_slots = number_of_slots - number_of_leases
lacking_ingestors = number_of_slots - number_of_ingestors lacking_ingestors = number_of_slots - number_of_ingestors
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...')
await 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
@@ -396,23 +398,23 @@ class Ingestor:
) )
self.logger.debug( self.logger.debug(
f"Active ingestors: {ingestors}, " f'Active ingestors: {ingestors}, '
f"Number of slots: {number_of_slots}, " f'Number of slots: {number_of_slots}, '
f"Number of leases: {number_of_leases}, " f'Number of leases: {number_of_leases}, '
f"Managed tags: {self.ingestor_manager.managed_tags}, " f'Managed tags: {self.ingestor_manager.managed_tags}, '
f"Managed servers: {self.ingestor_manager.opc_managers}" f'Managed servers: {self.ingestor_manager.opc_managers}'
) )
if not self.ingestor_manager.managed_tags: if not self.ingestor_manager.managed_tags:
# No slots acquired # No slots acquired
self.logger.info("No slots acquired in this loop") self.logger.info('No slots acquired in this loop')
# Update opc servers # Update opc servers
self.logger.info("Updating slot config...") self.logger.info('Updating slot config...')
self.ingestor_manager.update_slot_config() self.ingestor_manager.update_slot_config()
# Check OPC cycles # Check OPC cycles
self.logger.info("Checking OPC servers integrity...") self.logger.info('Checking OPC servers integrity...')
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...')
await self.update_ingestor_manager(current_managed_tags) await self.update_ingestor_manager(current_managed_tags)

View File

@@ -1,16 +1,18 @@
import json import json
import os
import traceback
from time import sleep from time import sleep
from pymongo import MongoClient
from kafka import KafkaProducer from kafka import KafkaProducer
from kafka.errors import NoBrokersAvailable from kafka.errors import NoBrokersAvailable
from pymongo import MongoClient
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.constants import now
from sientia_do.temporal.activities.base import BaseActivity
from sientia_do.observability.logger import Logger from sientia_do.observability.logger import Logger
import traceback from sientia_do.temporal.activities.base import BaseActivity
from sientia_do.temporal.constants import now
import ingestor.metrics as metrics import ingestor.metrics as metrics
import os
class DataManager(BaseActivity): class DataManager(BaseActivity):
@@ -85,68 +87,60 @@ class DataManager(BaseActivity):
- KAFKA_CONNECTION_STATUS: Set to 1 on successful connection, 0 on failure - KAFKA_CONNECTION_STATUS: Set to 1 on successful connection, 0 on failure
""" """
self.pod_id = os.getenv("HOSTNAME", "localhost") self.pod_id = os.getenv('HOSTNAME', 'localhost')
self.kafka_producer = None self.kafka_producer = None
self.export_to_kafka = export_to_kafka self.export_to_kafka = export_to_kafka
if self.export_to_kafka: if self.export_to_kafka:
for i in range(0, 3): for i in range(0, 3):
logger.info( logger.info(
f"Trying ({i}) to initializing DataManager with Kafka servers: {kafka_servers}" f'Trying ({i}) to initializing DataManager with Kafka servers: {kafka_servers}'
) )
try: try:
self.kafka_producer = KafkaProducer( self.kafka_producer = KafkaProducer(
bootstrap_servers=kafka_servers, bootstrap_servers=kafka_servers,
value_serializer=lambda v: json.dumps(v).encode( value_serializer=lambda v: json.dumps(v).encode(
"utf-8" 'utf-8'
), # Serialize JSON messages ), # Serialize JSON messages
key_serializer=lambda k: str( key_serializer=lambda k: str(k).encode('utf-8') if k else None,
k).encode("utf-8") if k else None,
) )
# Kafka connected # Kafka connected
metrics.KAFKA_CONNECTION_STATUS.labels( metrics.KAFKA_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(1)
pod_id=self.pod_id).set(1)
break break
except NoBrokersAvailable: except NoBrokersAvailable:
logger.error( logger.error(f'Kafka servers {kafka_servers} are not available. Retrying...')
f"Kafka servers {kafka_servers} are not available. Retrying..."
)
sleep(5) sleep(5)
else: else:
# Kafka not connected # Kafka not connected
metrics.KAFKA_CONNECTION_STATUS.labels( metrics.KAFKA_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(0)
pod_id=self.pod_id).set(0)
logger.error( logger.error(
f"Failed to connect to Kafka servers {kafka_servers} after 3 attempts." f'Failed to connect to Kafka servers {kafka_servers} after 3 attempts.'
) )
raise NoBrokersAvailable( raise NoBrokersAvailable(
f"Failed to connect to Kafka servers {kafka_servers} after 3 attempts." f'Failed to connect to Kafka servers {kafka_servers} after 3 attempts.'
) )
logger.info( logger.info(f'DataManager initialized with Kafka servers: {kafka_servers}')
f"DataManager initialized with Kafka servers: {kafka_servers}")
logger.info( logger.info(
f"Trying to initializing DataManager with MongoDB servers: {mongo_connection_string}" f'Trying to initializing DataManager with MongoDB servers: {mongo_connection_string}'
) )
self.connection_string = mongo_connection_string self.connection_string = mongo_connection_string
self.database = mongo_database self.database = mongo_database
self.mongo_client = MongoClient(self.connection_string) self.mongo_client: MongoClient = MongoClient(self.connection_string)
self.mongo_client.server_info() self.mongo_client.server_info()
self.metadata = metadata self.metadata = metadata
self.mongo_db = self.mongo_client[self.database] self.mongo_db = self.mongo_client[self.database]
logger.info( logger.info(f'DataManager initialized with MongoDB servers: {self.connection_string}')
f"DataManager initialized with MongoDB servers: {self.connection_string}"
)
BaseActivity.__init__(self, logger=logger, BaseActivity.__init__(
notification_handler=notification_handler, self, logger=logger, notification_handler=notification_handler, set_error_counter=True
set_error_counter=True) )
def shutdown(self): def shutdown(self):
""" """
@@ -165,27 +159,24 @@ class DataManager(BaseActivity):
self.kafka_producer.flush(timeout=10) self.kafka_producer.flush(timeout=10)
self.kafka_producer.close() self.kafka_producer.close()
# Mark as disconnected # Mark as disconnected
metrics.KAFKA_CONNECTION_STATUS.labels( metrics.KAFKA_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(0)
pod_id=self.pod_id).set(0)
except Exception as e: except Exception as e:
self.logger.error(f"Error closing Kafka producer: {e}") self.logger.error(f'Error closing Kafka producer: {e}')
else: else:
self.logger.warning( self.logger.warning('Kafka producer is already closed or not initialized.')
"Kafka producer is already closed or not initialized.")
if self.mongo_client: if self.mongo_client:
try: try:
self.mongo_client.close() self.mongo_client.close()
except Exception as e: except Exception as e:
self.logger.error(f"Error closing MongoDB client: {e}") self.logger.error(f'Error closing MongoDB client: {e}')
else: else:
self.logger.warning( self.logger.warning('MongoDB client is already closed or not initialized.')
"MongoDB client is already closed or not initialized.")
def __del__(self): def __del__(self):
self.shutdown() self.shutdown()
def delivery_report(self, msg: str): def delivery_report(self, msg):
""" """
Callback for successful Kafka message delivery reports. Callback for successful Kafka message delivery reports.
@@ -197,10 +188,10 @@ class DataManager(BaseActivity):
msg: Kafka message object containing delivery details msg: Kafka message object containing delivery details
""" """
self.logger.debug( self.logger.debug(
f"Record successfully produced to {msg.topic} [{msg.partition}] at offset {msg.offset}" f'Record successfully produced to {msg.topic} [{msg.partition}] at offset {msg.offset}'
) )
def delivery_error(self, err: str): def delivery_error(self, err):
""" """
Callback for Kafka message delivery error reports. Callback for Kafka message delivery error reports.
@@ -210,7 +201,7 @@ class DataManager(BaseActivity):
Args: Args:
err: Error information from the failed delivery attempt err: Error information from the failed delivery attempt
""" """
self.logger.error(f"Delivery failed for record : {err}") self.logger.error(f'Delivery failed for record : {err}')
def publish(self, topic: str, data: dict) -> None: def publish(self, topic: str, data: dict) -> None:
""" """
@@ -227,28 +218,24 @@ class DataManager(BaseActivity):
Exception: If there is an error during message delivery, it will be handled by the `delivery_error` callback. Exception: If there is an error during message delivery, it will be handled by the `delivery_error` callback.
""" """
if self.export_to_kafka: if self.export_to_kafka and self.kafka_producer:
try: try:
self.logger.debug(f'Publishing message to topic {topic}: {data}')
self.logger.debug(
f"Publishing message to topic {topic}: {data}")
self.kafka_producer.send(topic=topic, value=data).add_callback( self.kafka_producer.send(topic=topic, value=data).add_callback(
self.delivery_report self.delivery_report
).add_errback(self.delivery_error) ).add_errback(self.delivery_error)
self.kafka_producer.flush(timeout=10) self.kafka_producer.flush(timeout=10)
metrics.KAFKA_MESSAGES_SENT.labels( metrics.KAFKA_MESSAGES_SENT.labels(pod_id=self.pod_id, topic=topic).inc()
pod_id=self.pod_id, topic=topic).inc()
except Exception as e: except Exception as e:
metrics.KAFKA_MESSAGES_ERRORS.labels( metrics.KAFKA_MESSAGES_ERRORS.labels(pod_id=self.pod_id, topic=topic).inc()
pod_id=self.pod_id, topic=topic).inc()
trace = traceback.format_exc() trace = traceback.format_exc()
self.send_notification( self.send_notification(
metadata=self.metadata, metadata=self.metadata,
notification_id=f"KAFKA_PRODUCER_ERROR_{topic}", notification_id=f'KAFKA_PRODUCER_ERROR_{topic}',
message=f"Error publishing message to topic {topic}: {e}", message=f'Error publishing message to topic {topic}: {e}',
block="kafka_producer", block='kafka_producer',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=trace, attachment_content=trace,
) )
@@ -260,25 +247,22 @@ class DataManager(BaseActivity):
collection.insert_one( collection.insert_one(
{ {
**data, **data,
"inserted_at": now(), 'inserted_at': now(),
} }
) )
self.logger.debug( self.logger.debug(f'Message inserted into MongoDB collection {topic}: {data}')
f"Message inserted into MongoDB collection {topic}: {data}")
metrics.TAG_WRITTEN_COUNT.labels( metrics.TAG_WRITTEN_COUNT.labels(
pod_id=self.pod_id, pod_id=self.pod_id, tag_name=data['name'], collection_name=topic
tag_name=data["name"],
collection_name=topic
).inc() ).inc()
except Exception as e: except Exception as e:
trace = traceback.format_exc() trace = traceback.format_exc()
self.send_notification( self.send_notification(
metadata=self.metadata, metadata=self.metadata,
notification_id=f"MONGO_PRODUCER_ERROR_{topic}", notification_id=f'MONGO_PRODUCER_ERROR_{topic}',
message=f"Error inserting message to MongoDB: {e}", message=f'Error inserting message to MongoDB: {e}',
block="mongo_producer", block='mongo_producer',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=trace, attachment_content=trace,
) )

View File

@@ -1,15 +1,16 @@
import asyncio import asyncio
import traceback import traceback
from typing import Dict, List
from copy import deepcopy from copy import deepcopy
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.temporal.activities.base import BaseActivity
from sientia_do.observability.logger import Logger from sientia_do.observability.logger import Logger
from sientia_do.temporal.activities.base import BaseActivity
import ingestor.metrics as metrics
from ingestor.managers.data_manager import DataManager from ingestor.managers.data_manager import DataManager
from ingestor.managers.opc_manager import OpcManager from ingestor.managers.opc_manager import OpcManager
from ingestor.managers.resource_manager import ResourceManager from ingestor.managers.resource_manager import ResourceManager
import ingestor.metrics as metrics
class IngestorManager(BaseActivity): class IngestorManager(BaseActivity):
@@ -57,18 +58,24 @@ class IngestorManager(BaseActivity):
metadata (dict): Application metadata metadata (dict): Application metadata
""" """
def __init__(self, def __init__(
kafka_servers: str, redis_data: dict, self,
lease_ttl: int, heartbeat_ttl: int, kafka_servers: str,
poll_interval: int, mongo_connection_string: str, mongo_database: str, redis_data: dict,
lease_ttl: int,
heartbeat_ttl: int,
poll_interval: int,
mongo_connection_string: str,
mongo_database: str,
metadata: dict, metadata: dict,
logger: Logger, notification_handler: NotificationHandler, logger: Logger,
export_to_kafka: bool = False): notification_handler: NotificationHandler,
export_to_kafka: bool = False,
redis_host = redis_data.get('host') ):
redis_port = redis_data.get('port') redis_host: str = redis_data['host']
redis_username = redis_data.get('username', None) redis_port: int = int(redis_data['port'])
redis_password = redis_data.get('password', None) redis_username: str | None = redis_data.get('username', None)
redis_password: str | None = redis_data.get('password', None)
self.data_manager = DataManager( self.data_manager = DataManager(
kafka_servers=kafka_servers, kafka_servers=kafka_servers,
@@ -77,9 +84,9 @@ class IngestorManager(BaseActivity):
export_to_kafka=export_to_kafka, export_to_kafka=export_to_kafka,
metadata=metadata, metadata=metadata,
logger=logger, logger=logger,
notification_handler=notification_handler notification_handler=notification_handler,
) )
self.opc_managers = {} self.opc_managers: dict = {}
self.resource_manager = ResourceManager( self.resource_manager = ResourceManager(
host=redis_host, host=redis_host,
port=redis_port, port=redis_port,
@@ -93,14 +100,14 @@ class IngestorManager(BaseActivity):
) )
self.number_of_slots = 0 self.number_of_slots = 0
self.poll_interval = poll_interval self.poll_interval = poll_interval
self.managed_tags = {} self.managed_tags: dict = {}
self.opc_servers = {} self.opc_servers: dict = {}
self.metadata = metadata self.metadata = metadata
BaseActivity.__init__(self, logger=logger, BaseActivity.__init__(
notification_handler=notification_handler, self, logger=logger, notification_handler=notification_handler, set_error_counter=True
set_error_counter=True) )
async def initialize_opc_from_config(self, server_config: dict) -> OpcManager | None: async def initialize_opc_from_config(self, server_config: dict) -> OpcManager | None:
""" """
@@ -130,8 +137,7 @@ class IngestorManager(BaseActivity):
""" """
try: try:
self.logger.info( self.logger.info(f'Initializing OpcManager at {server_config["url"]}')
f"Initializing OpcManager at {server_config['url']}")
manager = OpcManager( manager = OpcManager(
name=server_config['name'], name=server_config['name'],
url=server_config['url'], url=server_config['url'],
@@ -142,7 +148,7 @@ class IngestorManager(BaseActivity):
metadata=self.metadata, metadata=self.metadata,
cert_path=server_config.get('cert_path'), cert_path=server_config.get('cert_path'),
private_key_path=server_config.get('private_key_path'), private_key_path=server_config.get('private_key_path'),
server_cert_path=server_config.get('server_cert_path') server_cert_path=server_config.get('server_cert_path'),
) )
manager.config = server_config manager.config = server_config
@@ -153,9 +159,9 @@ class IngestorManager(BaseActivity):
metadata=self.metadata, metadata=self.metadata,
notification_id=f'OPC_CONNECTION_ERROR_{server_config["name"]}', notification_id=f'OPC_CONNECTION_ERROR_{server_config["name"]}',
message=f'Error initializing OPC manager: {e}', message=f'Error initializing OPC manager: {e}',
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=trace attachment_content=trace,
) )
self.logger.error(trace) self.logger.error(trace)
@@ -216,49 +222,41 @@ class IngestorManager(BaseActivity):
server_config.pop('tags', None) server_config.pop('tags', None)
server_instance = self.opc_managers.get(server, None) server_instance = self.opc_managers.get(server, None)
if server_instance is None: if server_instance is None:
self.logger.info( self.logger.info(f'Initializing OPC manager for server {server}')
f"Initializing OPC manager for server {server}" server_instance = await self.initialize_opc_from_config(server_config)
)
server_instance = await self.initialize_opc_from_config(
server_config
)
elif server_instance.config != server_config: elif server_instance.config != server_config:
self.logger.warning( self.logger.warning(f'Reinitializing OPC manager for server {server}')
f"Reinitializing OPC manager for server {server}"
)
server_instance.disconnect() server_instance.disconnect()
del self.opc_managers[server] del self.opc_managers[server]
server_instance = await self.initialize_opc_from_config( server_instance = await self.initialize_opc_from_config(server_config)
server_config
)
else: else:
self.logger.debug( self.logger.debug(
f"OPC manager for server {server} is already initialized and up to date" f'OPC manager for server {server} is already initialized and up to date'
) )
if server_instance is not None: if server_instance is not None:
self.opc_managers[server] = server_instance self.opc_managers[server] = server_instance
else: else:
self.logger.warning( self.logger.warning(
f"Failed to initialize OPC manager for server {server}, " f'Failed to initialize OPC manager for server {server}, '
f"removing server from managed tags." f'removing server from managed tags.'
) )
_a = [self.managed_tags[slot].pop(server, None) _a = [
for slot, _value in current_managed_tags.items()] self.managed_tags[slot].pop(server, None)
for slot, _value in current_managed_tags.items()
]
servers = list(self.opc_managers.keys()) servers = list(self.opc_managers.keys())
for server in servers: for server in servers:
if server not in registered_servers: if server not in registered_servers:
self.logger.warning( self.logger.warning(
f"Server {server} not found in managed tags. " f'Server {server} not found in managed tags. Desconnecting from server.'
f"Desconnecting from server."
) )
await 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(pod_id=self.pod_id).set(len(self.opc_managers))
pod_id=self.pod_id).set(len(self.opc_managers))
def check_opc_servers_integrity(self): def check_opc_servers_integrity(self):
""" """
@@ -280,16 +278,12 @@ class IngestorManager(BaseActivity):
is_lost = opc_manager.check_opc_listenning() is_lost = opc_manager.check_opc_listenning()
if is_lost: if is_lost:
self.logger.warning( self.logger.warning(f'OPC server {server} is lost. Server will be disconnected.')
f"OPC server {server} is lost. "
f"Server will be disconnected."
)
for slot, _config in self.managed_tags.items(): for slot, _config in self.managed_tags.items():
self.managed_tags[slot].pop(server, None) self.managed_tags[slot].pop(server, None)
metrics.OPC_MANAGERS_ACTIVE.labels( metrics.OPC_MANAGERS_ACTIVE.labels(pod_id=self.pod_id).set(len(self.opc_managers))
pod_id=self.pod_id).set(len(self.opc_managers))
def declare_active(self): def declare_active(self):
""" """
@@ -306,7 +300,7 @@ class IngestorManager(BaseActivity):
self.resource_manager.ingestor_heartbeat() self.resource_manager.ingestor_heartbeat()
def get_active_ingestors(self) -> List[str]: def get_active_ingestors(self) -> list[str]:
""" """
Retrieve a list of active ingestors. Retrieve a list of active ingestors.
@@ -363,7 +357,7 @@ class IngestorManager(BaseActivity):
metrics.SLOTS_TOTAL.set(self.number_of_slots) metrics.SLOTS_TOTAL.set(self.number_of_slots)
return self.number_of_slots return self.number_of_slots
def get_slot_leases(self, max_slots: int = 1) -> Dict: def get_slot_leases(self, max_slots: int = 1) -> dict:
""" """
Acquires a specified number of resource slots by leasing them from the resource manager. Acquires a specified number of resource slots by leasing them from the resource manager.
@@ -394,7 +388,7 @@ class IngestorManager(BaseActivity):
acquired = {} acquired = {}
for i in range(1, self.number_of_slots + 1): for i in range(1, self.number_of_slots + 1):
if self.resource_manager.lease_tag(str(i)): if self.resource_manager.lease_tag(str(i)):
self.logger.info(f"Leased slot {i}") self.logger.info(f'Leased slot {i}')
slots = self.resource_manager.get_tag_slot(str(i)) slots = self.resource_manager.get_tag_slot(str(i))
if slots is None: if slots is None:
continue continue
@@ -403,17 +397,14 @@ class IngestorManager(BaseActivity):
if len(acquired) >= max_slots: if len(acquired) >= max_slots:
self.managed_tags.update(acquired) self.managed_tags.update(acquired)
metrics.SLOTS_MANAGED.labels( metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set(len(self.managed_tags))
pod_id=self.pod_id).set(len(self.managed_tags))
return acquired return acquired
self.logger.warning( self.logger.warning(
f"Unable to acquire {max_slots} slots. " f'Unable to acquire {max_slots} slots. Only {acquired} slots were leased.'
f"Only {acquired} slots were leased."
) )
self.managed_tags.update(acquired) self.managed_tags.update(acquired)
metrics.SLOTS_MANAGED.labels( metrics.SLOTS_MANAGED.labels(pod_id=self.pod_id).set(len(self.managed_tags))
pod_id=self.pod_id).set(len(self.managed_tags))
return acquired return acquired
async def unsubscribe_slot(self, slot: str): async def unsubscribe_slot(self, slot: str):
@@ -466,8 +457,7 @@ class IngestorManager(BaseActivity):
- Logs informational messages for updated slot configurations. - Logs informational messages for updated slot configurations.
""" """
removed_slots = [] removed_slots: list[str] = []
update = {}
for slot, _slot_config in self.managed_tags.items(): for slot, _slot_config in self.managed_tags.items():
self.resource_manager.renew_tag_lease(slot) self.resource_manager.renew_tag_lease(slot)
update = self.resource_manager.get_tag_slot(slot) update = self.resource_manager.get_tag_slot(slot)
@@ -480,7 +470,7 @@ class IngestorManager(BaseActivity):
for slot in removed_slots: for slot in removed_slots:
self.managed_tags.pop(slot, None) self.managed_tags.pop(slot, None)
def drop_slot_leases(self, ids: List[str]) -> None: def drop_slot_leases(self, ids: list[str]) -> None:
""" """
Releases the leases associated with the specified slot IDs. Releases the leases associated with the specified slot IDs.
@@ -541,58 +531,44 @@ class IngestorManager(BaseActivity):
- Updates error metrics and notifications - Updates error metrics and notifications
""" """
self.logger.info( self.logger.info(f'Subscribing to tags from {slot}:{server}')
f"Subscribing to tags from {slot}:{server}"
)
tags_to_sub = server_config.get('tags') tags_to_sub = server_config.get('tags')
if server not in self.opc_managers: if server not in self.opc_managers:
self.logger.error( self.logger.error(f'Server {server} not found in opc_managers.')
f"Server {server} not found in opc_managers."
)
return 1 return 1
if slot not in self.opc_managers[server].subscriptions: if slot not in self.opc_managers[server].subscriptions:
try: try:
await self.opc_managers[server].create_subscription( await self.opc_managers[server].create_subscription(slot)
slot
)
except Exception as e: except Exception as e:
self.logger.error( self.logger.error(f'Failed to create subscription for slot {slot}: {e}')
f"Failed to create subscription for slot {slot}: {e}"
)
return 2 return 2
try: try:
self.logger.info( self.logger.info(tags_to_sub)
tags_to_sub
)
await 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(tags_to_sub)
tags_to_sub
)
except Exception as e: except Exception as e:
metrics.OPC_SUBSCRIPTION_ERRORS.labels( metrics.OPC_SUBSCRIPTION_ERRORS.labels(
pod_id=self.pod_id, server=server, slot=slot).inc() pod_id=self.pod_id, server=server, slot=slot
).inc()
trace = traceback.format_exc() trace = traceback.format_exc()
self.send_notification( self.send_notification(
metadata=self.metadata, metadata=self.metadata,
notification_id=f'OPC_SUBSCRIPTION_ERROR_{slot}:{server}', notification_id=f'OPC_SUBSCRIPTION_ERROR_{slot}:{server}',
message=f'Failed to subscribe to tags from {slot}:{server}\n{tags}: {e}', message=f'Failed to subscribe to tags from {slot}:{server}\n{tags}: {e}',
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=trace attachment_content=trace,
) )
self.logger.error(trace) self.logger.error(trace)
self.logger.warning( self.logger.warning(f'Removing subscription from server {server} for slot {slot}')
"Removing subscription from server "
f"{server} for slot {slot}"
)
await self.opc_managers[server].unsubscribe(slot) await self.opc_managers[server].unsubscribe(slot)
return 2 return 2
return 0 return 0
async 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.
@@ -622,9 +598,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 = await self.manage_server( response = await self.manage_server(slot, server, server_config, tags)
slot, server, server_config, tags
)
if response == 2: if response == 2:
to_remove.append([slot, server]) to_remove.append([slot, server])

View File

@@ -1,15 +1,16 @@
import json import json
import asyncio
from pathlib import Path from pathlib import Path
from asyncua.crypto.security_policies import SecurityPolicyBasic256
from asyncua import Client from asyncua import Client
from sientia_do.notifications.models import NotificationLevel from asyncua.crypto.security_policies import SecurityPolicyBasic256
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.notifications.models import NotificationLevel
from sientia_do.observability.logger import Logger from sientia_do.observability.logger import Logger
from sientia_do.temporal.constants import OPC_TIMEZONE, DATETIME_FORMAT_WITH_TZ from sientia_do.temporal.activities.base import BaseActivity
from ingestor.managers.data_manager import DataManager from sientia_do.temporal.constants import DATETIME_FORMAT_WITH_TZ, OPC_TIMEZONE
import ingestor.metrics as metrics import ingestor.metrics as metrics
from ingestor.managers.data_manager import DataManager
class OpcManager(BaseActivity): class OpcManager(BaseActivity):
@@ -54,31 +55,41 @@ class OpcManager(BaseActivity):
metadata (dict): Application metadata metadata (dict): Application metadata
""" """
def __init__(self, name: str, url: str, data_manager: DataManager, logger: Logger, def __init__(
server_uri: str, notification_handler: NotificationHandler, metadata: dict, self,
cert_path: str = None, private_key_path: str = None, server_cert_path: str = None): name: str,
url: str,
data_manager: DataManager,
logger: Logger,
server_uri: str,
notification_handler: NotificationHandler,
metadata: dict,
cert_path: str | None = None,
private_key_path: str | None = None,
server_cert_path: str | None = None,
):
self.url = url self.url = url
self.name = name self.name = name
self.server_uri = server_uri self.server_uri = server_uri
self.data_queue = {} self.data_queue: dict = {}
self.non_receive_count = 0 self.non_receive_count = 0
self.client = None self.client: Client | None = None
self.cert_path = cert_path self.cert_path = cert_path
self.private_key_path = private_key_path self.private_key_path = private_key_path
self.server_cert_path = server_cert_path self.server_cert_path = server_cert_path
self.nodes = {} self.nodes: dict = {}
self.subscriptions = {} self.subscriptions: dict = {}
self.data_manager = data_manager self.data_manager = data_manager
self.metadata = metadata self.metadata = metadata
BaseActivity.__init__(self, logger=logger, BaseActivity.__init__(
notification_handler=notification_handler, self, logger=logger, notification_handler=notification_handler, set_error_counter=True
set_error_counter=True) )
metrics.OPC_CONNECTION_STATUS.labels( metrics.OPC_CONNECTION_STATUS.labels(
pod_id=self.pod_id, server_name=self.name, server_url=self.url).set(0) pod_id=self.pod_id, server_name=self.name, server_url=self.url
metrics.OPC_TAGS_SUBSCRIBED.labels( ).set(0)
pod_id=self.pod_id, server_name=self.name).set(0) metrics.OPC_TAGS_SUBSCRIBED.labels(pod_id=self.pod_id, server_name=self.name).set(0)
def __str__(self): def __str__(self):
""" """
@@ -87,8 +98,10 @@ class OpcManager(BaseActivity):
Returns: Returns:
str: Human-readable representation showing server details and current state. str: Human-readable representation showing server details and current state.
""" """
return f"OpcManager(name={self.name}, url={self.url}, server_uri={self.server_uri})\n" \ return (
f"nodes={self.nodes}, subscriptions={self.subscriptions}" f'OpcManager(name={self.name}, url={self.url}, server_uri={self.server_uri})\n'
f'nodes={self.nodes}, subscriptions={self.subscriptions}'
)
async def shutdown(self): async def shutdown(self):
""" """
@@ -103,10 +116,9 @@ class OpcManager(BaseActivity):
and ensure clean disconnection from OPC servers. and ensure clean disconnection from OPC servers.
""" """
try: try:
await self.disconnect() await self.disconnect()
except Exception as e: except Exception as e:
self.logger.error(f"Error during cleanup: {e}") self.logger.error(f'Error during cleanup: {e}')
async def set_security(self): async def set_security(self):
""" """
@@ -133,19 +145,20 @@ class OpcManager(BaseActivity):
if not all([self.cert_path, self.private_key_path]): if not all([self.cert_path, self.private_key_path]):
raise ValueError( raise ValueError(
"Certificate and private key paths must be provided for secure connection.") 'Certificate and private key paths must be provided for secure connection.'
cert = Path(self.cert_path) )
private_key = Path(self.private_key_path) cert = Path(self.cert_path) if self.cert_path else None
server_cert = Path( private_key = Path(self.private_key_path) if self.private_key_path else None
self.server_cert_path) if self.server_cert_path else None server_cert = Path(self.server_cert_path) if self.server_cert_path else None
if self.client:
await self.client.set_application_uri(self.server_uri) await self.client.set_application_uri(self.server_uri)
self.logger.info('Setting security...') self.logger.info('Setting security...')
await 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),
) )
await self.client.set_secure_channel_timeout(10000000) await self.client.set_secure_channel_timeout(10000000)
await self.client.set_session_timeout(10000000) await self.client.set_session_timeout(10000000)
@@ -172,24 +185,25 @@ class OpcManager(BaseActivity):
- OPC_CONNECTION_STATUS: Set to 1 on successful connection - OPC_CONNECTION_STATUS: Set to 1 on successful connection
""" """
metrics.OPC_CONNECTIONS_TOTAL.labels( metrics.OPC_CONNECTIONS_TOTAL.labels(pod_id=self.pod_id, server_name=self.name).inc()
pod_id=self.pod_id, server_name=self.name).inc()
try: try:
self.client = Client(self.url, watchdog_intervall=3600000) self.client = Client(self.url, watchdog_intervall=3600000)
assert self.client is not None # Informa ao mypy que client não é None
self.client.name = self.pod_id self.client.name = self.pod_id
if self.cert_path: if self.cert_path:
await 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}...')
await 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.')
except Exception as e: except Exception as e:
metrics.OPC_CONNECTION_STATUS.labels( metrics.OPC_CONNECTION_STATUS.labels(
pod_id=self.pod_id, server_name=self.name, server_url=self.url).set(0) pod_id=self.pod_id, server_name=self.name, server_url=self.url
metrics.OPC_CONNECTIONS_FAILED.labels( ).set(0)
pod_id=self.pod_id, server_name=self.name).inc() metrics.OPC_CONNECTIONS_FAILED.labels(pod_id=self.pod_id, server_name=self.name).inc()
self.logger.error(f"Failed to connect to {self.name}: {e}") self.logger.error(f'Failed to connect to {self.name}: {e}')
raise raise
async def create_subscription(self, name: str, period: int = 500): async def create_subscription(self, name: str, period: int = 500):
@@ -214,16 +228,16 @@ class OpcManager(BaseActivity):
""" """
if not self.client: if not self.client:
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] = await 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()
except Exception as e: except Exception as e:
self.logger.error( self.logger.error(f'Failed to create subscription {name} on {self.name}: {e}')
f"Failed to create subscription {name} on {self.name}: {e}")
raise raise
async def subscribe(self, subscription: str, nodes: dict, collect_period: int): async def subscribe(self, subscription: str, nodes: dict, collect_period: int):
@@ -252,23 +266,24 @@ class OpcManager(BaseActivity):
""" """
if not self.subscriptions.get(subscription): if not self.subscriptions.get(subscription):
raise ValueError( raise ValueError('Subscription not created. Call create_subscription first.')
"Subscription not created. Call create_subscription first.")
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}')
assert self.client is not None # Informa ao mypy que client não é None
addr_nodes = [self.client.get_node(n) for n in nodes] addr_nodes = [self.client.get_node(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}')
metrics.OPC_TAGS_SUBSCRIBED.labels( metrics.OPC_TAGS_SUBSCRIBED.labels(pod_id=self.pod_id, server_name=self.name).set(
pod_id=self.pod_id, server_name=self.name).set(len(self.nodes)) len(self.nodes)
)
self.collect_period = collect_period self.collect_period = collect_period
for node, config in self.nodes.items(): for node, config in self.nodes.items():
self.nodes[node]['cycle_rule'] = { self.nodes[node]['cycle_rule'] = {
'cycle_increment': collect_period*1000/float(config['frequency']), 'cycle_increment': collect_period * 1000 / float(config['frequency']),
'cycle_count': 0 'cycle_count': 0,
} }
await self.subscriptions[subscription].subscribe_data_change(addr_nodes) await self.subscriptions[subscription].subscribe_data_change(addr_nodes)
@@ -294,12 +309,11 @@ class OpcManager(BaseActivity):
""" """
if not self.subscriptions.get(subscription): if not self.subscriptions.get(subscription):
self.logger.warning( self.logger.warning(f"Subscription '{subscription}' not found. Cannot unsubscribe.")
f"Subscription '{subscription}' not found. Cannot unsubscribe.")
return return
await 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}.')
async def disconnect(self): async def disconnect(self):
""" """
@@ -322,28 +336,27 @@ class OpcManager(BaseActivity):
self.logger.warning('Disconnecting from OPC server') self.logger.warning('Disconnecting from OPC server')
if self.client is None: if self.client is None:
self.logger.warning("Client already disconnected.") self.logger.warning('Client already disconnected.')
return return
try: try:
for sub in self.subscriptions: for sub in self.subscriptions:
await self.subscriptions[sub].delete() 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:
await 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}")
finally: finally:
del self.client del self.client
self.client = None self.client = None
metrics.OPC_CONNECTION_STATUS.labels( metrics.OPC_CONNECTION_STATUS.labels(
pod_id=self.pod_id, server_name=self.name, server_url=self.url).set(0) pod_id=self.pod_id, server_name=self.name, server_url=self.url
metrics.OPC_TAGS_SUBSCRIBED.labels( ).set(0)
pod_id=self.pod_id, server_name=self.name).set(0) metrics.OPC_TAGS_SUBSCRIBED.labels(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.')
async def datachange_notification(self, node, _val, data): async def datachange_notification(self, node, _val, data):
""" """
@@ -371,28 +384,27 @@ class OpcManager(BaseActivity):
monitored_item = data.monitored_item monitored_item = data.monitored_item
value = monitored_item.Value.Value.Value value = monitored_item.Value.Value.Value
# source_timestamp # source_timestamp
source_timestamp = monitored_item.Value.SourceTimestamp.replace( source_timestamp = monitored_item.Value.SourceTimestamp.replace(tzinfo=OPC_TIMEZONE)
tzinfo=OPC_TIMEZONE)
tag = str(node) tag = str(node)
self.logger.debug( self.logger.debug(
f"Data change notification received for tag:" f'Data change notification received for tag:'
f"{tag} after {self.nodes[tag]['cycle_rule']['cycle_count']} cycles") f'{tag} after {self.nodes[tag]["cycle_rule"]["cycle_count"]} cycles'
)
self.nodes[tag]['cycle_rule']['cycle_count'] = 0 self.nodes[tag]['cycle_rule']['cycle_count'] = 0
self.non_receive_count = 0 self.non_receive_count = 0
metrics.OPC_CYCLES_WITHOUT_DATA.labels( metrics.OPC_CYCLES_WITHOUT_DATA.labels(pod_id=self.pod_id, server_name=self.name).set(0)
pod_id=self.pod_id, server_name=self.name).set(0)
data = { data = {
'tag': tag, 'tag': tag,
'name': self.nodes[str(node)]['tag_name'], 'name': self.nodes[str(node)]['tag_name'],
'timestamp': source_timestamp.strftime(DATETIME_FORMAT_WITH_TZ), 'timestamp': source_timestamp.strftime(DATETIME_FORMAT_WITH_TZ),
'value': value 'value': value,
} }
_a = [self.data_manager.publish(e, data) for topic in self.nodes[tag]['topics']:
for e in self.nodes[tag]['topics']] self.data_manager.publish(topic, data)
def check_cycles(self): def check_cycles(self):
""" """
@@ -408,8 +420,7 @@ class OpcManager(BaseActivity):
- Sends warning notifications for nodes exceeding cycle thresholds - Sends warning notifications for nodes exceeding cycle thresholds
""" """
for node, config in self.nodes.items(): for node, config in self.nodes.items():
self.nodes[node]['cycle_rule']['cycle_count'] += config[ self.nodes[node]['cycle_rule']['cycle_count'] += config['cycle_rule']['cycle_increment']
'cycle_rule']['cycle_increment']
if self.nodes[node]['cycle_rule']['cycle_count'] >= 5: if self.nodes[node]['cycle_rule']['cycle_count'] >= 5:
name = config['tag_name'] name = config['tag_name']
cycles = self.nodes[node]['cycle_rule']['cycle_count'] cycles = self.nodes[node]['cycle_rule']['cycle_count']
@@ -417,8 +428,8 @@ class OpcManager(BaseActivity):
metadata=self.metadata, metadata=self.metadata,
notification_id=f'TAG_{node}:{name}_LISTENNING_STOPPED', notification_id=f'TAG_{node}:{name}_LISTENNING_STOPPED',
message=f'{cycles} cycles without receive from {node}:{name}', message=f'{cycles} cycles without receive from {node}:{name}',
block="opc_manager", block='opc_manager',
level=NotificationLevel.WARNING level=NotificationLevel.WARNING,
) )
def check_opc_listenning(self) -> bool: def check_opc_listenning(self) -> bool:
@@ -441,26 +452,26 @@ class OpcManager(BaseActivity):
""" """
self.non_receive_count += 1 self.non_receive_count += 1
metrics.OPC_CYCLES_WITHOUT_DATA.labels( metrics.OPC_CYCLES_WITHOUT_DATA.labels(pod_id=self.pod_id, server_name=self.name).set(
pod_id=self.pod_id, server_name=self.name).set(self.non_receive_count) self.non_receive_count
)
if self.non_receive_count >= 5: if self.non_receive_count >= 5:
self.send_notification( self.send_notification(
metadata=self.metadata, metadata=self.metadata,
notification_id=f'OPC_LISTENNING_STOPPED__{self.name}', notification_id=f'OPC_LISTENNING_STOPPED__{self.name}',
message=f'{self.non_receive_count} cycles without ' message=f'{self.non_receive_count} cycles without '
f'receive from OPC {self.name}. Tags: {json.dumps(self.nodes)}', f'receive from OPC {self.name}. Tags: {json.dumps(self.nodes)}',
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR level=NotificationLevel.ERROR,
) )
if self.non_receive_count >= 15: if self.non_receive_count >= 15:
metrics.OPC_RECONNECTIONS_TOTAL.labels( metrics.OPC_RECONNECTIONS_TOTAL.labels(pod_id=self.pod_id, server_name=self.name).inc()
pod_id=self.pod_id, server_name=self.name).inc()
self.send_notification( self.send_notification(
metadata=self.metadata, metadata=self.metadata,
notification_id=f'OPC_CONNECTION_RETRY__{self.name}', notification_id=f'OPC_CONNECTION_RETRY__{self.name}',
message=f'Retrying to connect to server {self.name}', message=f'Retrying to connect to server {self.name}',
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR level=NotificationLevel.ERROR,
) )
return True return True
return False return False

View File

@@ -1,13 +1,14 @@
import json import json
from typing import List
from redis import Redis
from time import time from time import time
import ingestor.metrics as metrics
from redis import Redis
from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler from sientia_do.notifications.handlers import CoreNotificationHandler as NotificationHandler
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from sientia_do.observability.logger import Logger from sientia_do.observability.logger import Logger
from sientia_do.temporal.activities.base import BaseActivity from sientia_do.temporal.activities.base import BaseActivity
import ingestor.metrics as metrics
class ResourceManager(BaseActivity): class ResourceManager(BaseActivity):
""" """
@@ -80,9 +81,9 @@ class ResourceManager(BaseActivity):
Metrics: Metrics:
- REDIS_CONNECTION_STATUS: Set to 1 on successful connection, 0 on failure - REDIS_CONNECTION_STATUS: Set to 1 on successful connection, 0 on failure
""" """
BaseActivity.__init__(self, logger=logger, BaseActivity.__init__(
notification_handler=notification_handler, self, logger=logger, notification_handler=notification_handler, set_error_counter=True
set_error_counter=True) )
try: try:
self.redis = Redis( self.redis = Redis(
host=host, host=host,
@@ -94,7 +95,7 @@ class ResourceManager(BaseActivity):
self.redis.ping() self.redis.ping()
metrics.REDIS_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(1) metrics.REDIS_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(1)
except Exception as e: except Exception as e:
self.logger.error(f"Failed to connect to Redis: {e}") self.logger.error(f'Failed to connect to Redis: {e}')
metrics.REDIS_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(0) metrics.REDIS_CONNECTION_STATUS.labels(pod_id=self.pod_id).set(0)
raise raise
@@ -146,14 +147,14 @@ class ResourceManager(BaseActivity):
).inc() ).inc()
self.send_notification( self.send_notification(
metadata=self.metadata, metadata=self.metadata,
notification_id=f"REDIS_OPERATION_ERROR_{operation_name}", notification_id=f'REDIS_OPERATION_ERROR_{operation_name}',
message=f"Error in Redis operation '{operation_name}': {e}", message=f"Error in Redis operation '{operation_name}': {e}",
block="redis_manager", block='redis_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
) )
raise raise
def get(self, key: str) -> dict: def get(self, key: str) -> dict | None:
""" """
Retrieve a value from Redis by its key and return it as a dictionary. Retrieve a value from Redis by its key and return it as a dictionary.
@@ -172,10 +173,10 @@ class ResourceManager(BaseActivity):
- REDIS_OPERATIONS_DURATION: Records timing for get operations - REDIS_OPERATIONS_DURATION: Records timing for get operations
""" """
history = self._execute_redis_op("get", self.redis.get, key) history = self._execute_redis_op('get', self.redis.get, key)
return json.loads(history) if history else None return json.loads(history) if history else None
def get_tag_slot(self, id: str) -> dict: def get_tag_slot(self, tag_id: str) -> dict | None:
""" """
Retrieve the tag slot information for a given ID. Retrieve the tag slot information for a given ID.
@@ -193,7 +194,7 @@ class ResourceManager(BaseActivity):
and delegates to the get() method for the actual Redis operation. and delegates to the get() method for the actual Redis operation.
""" """
return self.get(f"slot:opc_tags:{id}") return self.get(f'slot:opc_tags:{tag_id}')
def ingestor_heartbeat(self) -> None: def ingestor_heartbeat(self) -> None:
""" """
@@ -215,9 +216,9 @@ class ResourceManager(BaseActivity):
""" """
self._execute_redis_op( self._execute_redis_op(
"set", 'set',
self.redis.set, self.redis.set,
f"heartbeat:ingestor:{self.pod_id}", f'heartbeat:ingestor:{self.pod_id}',
1, 1,
ex=self.heartbeat_ttl, ex=self.heartbeat_ttl,
) )
@@ -249,9 +250,9 @@ class ResourceManager(BaseActivity):
""" """
return self._execute_redis_op( return self._execute_redis_op(
"set_nx", 'set_nx',
self.redis.set, self.redis.set,
f"lease:opc_tags:{tag_id}", f'lease:opc_tags:{tag_id}',
self.pod_id, self.pod_id,
nx=True, nx=True,
ex=self.lease_ttl, ex=self.lease_ttl,
@@ -282,12 +283,10 @@ class ResourceManager(BaseActivity):
- REDIS_OPERATIONS_DURATION: Records timing for renewal operations - REDIS_OPERATIONS_DURATION: Records timing for renewal operations
""" """
current = self._execute_redis_op( current = self._execute_redis_op('get', self.redis.get, f'lease:opc_tags:{tag_id}')
"get", self.redis.get, f"lease:opc_tags:{tag_id}"
)
if current == self.pod_id: if current == self.pod_id:
self._execute_redis_op( self._execute_redis_op(
"expire", self.redis.expire, f"lease:opc_tags:{tag_id}", self.lease_ttl 'expire', self.redis.expire, f'lease:opc_tags:{tag_id}', self.lease_ttl
) )
return True return True
return False return False
@@ -313,10 +312,9 @@ class ResourceManager(BaseActivity):
- REDIS_OPERATIONS_DURATION: Records timing for lease dropping operations - REDIS_OPERATIONS_DURATION: Records timing for lease dropping operations
""" """
self._execute_redis_op("delete", self.redis.delete, self._execute_redis_op('delete', self.redis.delete, f'lease:opc_tags:{tag_id}')
f"lease:opc_tags:{tag_id}")
def get_all_ingestors(self) -> List[str]: def get_all_ingestors(self) -> list[str]:
""" """
Retrieves all active ingestors from Redis. Retrieves all active ingestors from Redis.
@@ -338,9 +336,9 @@ class ResourceManager(BaseActivity):
- REDIS_OPERATIONS_DURATION: Records timing for ingestor discovery - REDIS_OPERATIONS_DURATION: Records timing for ingestor discovery
""" """
return self._execute_redis_op("keys", self.redis.keys, "heartbeat:ingestor:*") return self._execute_redis_op('keys', self.redis.keys, 'heartbeat:ingestor:*')
def get_all_slots(self) -> List[str]: def get_all_slots(self) -> list[str]:
""" """
Retrieves all available slots from Redis. Retrieves all available slots from Redis.
@@ -362,9 +360,9 @@ class ResourceManager(BaseActivity):
- REDIS_OPERATIONS_DURATION: Records timing for slot discovery - REDIS_OPERATIONS_DURATION: Records timing for slot discovery
""" """
return self._execute_redis_op("keys", self.redis.keys, "slot:opc_tags:*") return self._execute_redis_op('keys', self.redis.keys, 'slot:opc_tags:*')
def get_all_leases(self) -> List[str]: def get_all_leases(self) -> list[str]:
""" """
Retrieves all active leases from Redis. Retrieves all active leases from Redis.
@@ -386,4 +384,4 @@ class ResourceManager(BaseActivity):
- REDIS_OPERATIONS_DURATION: Records timing for lease discovery - REDIS_OPERATIONS_DURATION: Records timing for lease discovery
""" """
return self._execute_redis_op("keys", self.redis.keys, "lease:opc_tags:*") return self._execute_redis_op('keys', self.redis.keys, 'lease:opc_tags:*')

View File

@@ -17,157 +17,157 @@ labels for multi-dimensional analysis and alerting.
from prometheus_client import Counter, Gauge, Histogram from prometheus_client import Counter, Gauge, Histogram
# Metric label definitions for consistent labeling across all metrics # Metric label definitions for consistent labeling across all metrics
POD_ID_LABEL = ["pod_id"] POD_ID_LABEL = ['pod_id']
SERVER_LABELS = ["pod_id", "server_name", "server_url"] SERVER_LABELS = ['pod_id', 'server_name', 'server_url']
KAFKA_LABELS = ["pod_id", "topic"] KAFKA_LABELS = ['pod_id', 'topic']
REDIS_LABELS = ["pod_id", "operation"] REDIS_LABELS = ['pod_id', 'operation']
NOTIFICATION_LABELS = ["pod_id", "level", "block"] NOTIFICATION_LABELS = ['pod_id', 'level', 'block']
MAIN_LABELS = ["pod_id"] MAIN_LABELS = ['pod_id']
# --- Reliability Metrics --- # --- Reliability Metrics ---
TAG_WRITTEN_COUNT = Counter( TAG_WRITTEN_COUNT = Counter(
"ingestor_tag_written_count", 'ingestor_tag_written_count',
"Number of writing process to the collection", 'Number of writing process to the collection',
[*MAIN_LABELS, "tag_name", "collection_name"], [*MAIN_LABELS, 'tag_name', 'collection_name'],
) )
# --- General Application Metrics --- # --- General Application Metrics ---
APP_LOOP_COUNT = Counter( APP_LOOP_COUNT = Counter(
"app_main_loop_total", 'app_main_loop_total',
"Total number of times the application main loop has run", 'Total number of times the application main loop has run',
POD_ID_LABEL, POD_ID_LABEL,
) )
APP_LOOP_DURATION = Histogram( APP_LOOP_DURATION = Histogram(
"app_main_loop_duration_seconds", 'app_main_loop_duration_seconds',
"Duration of the application main loop in seconds", 'Duration of the application main loop in seconds',
POD_ID_LABEL, POD_ID_LABEL,
) )
APP_ERRORS_TOTAL = Counter( APP_ERRORS_TOTAL = Counter(
"app_errors_total", 'app_errors_total',
"Total number of unhandled errors in the main loop", 'Total number of unhandled errors in the main loop',
POD_ID_LABEL, POD_ID_LABEL,
) )
APP_UP = Gauge( APP_UP = Gauge(
"app_up", 'app_up',
"Indicates if the application is running (1) or shutting down (0)", 'Indicates if the application is running (1) or shutting down (0)',
POD_ID_LABEL, POD_ID_LABEL,
) )
# --- Ingestor Manager Metrics --- # --- Ingestor Manager Metrics ---
ACTIVE_INGESTORS = Gauge( ACTIVE_INGESTORS = Gauge(
"ingestor_active_total", 'ingestor_active_total',
"Number of active ingestors reported by Redis", 'Number of active ingestors reported by Redis',
) )
SLOTS_TOTAL = Gauge( SLOTS_TOTAL = Gauge(
"ingestor_slots_total", 'ingestor_slots_total',
"Total number of slots configured in Redis", 'Total number of slots configured in Redis',
) )
LEASES_TOTAL = Gauge( LEASES_TOTAL = Gauge(
"ingestor_leases_total", 'ingestor_leases_total',
"Total number of leases (allocated slots) in Redis", 'Total number of leases (allocated slots) in Redis',
) )
SLOTS_MANAGED = Gauge( SLOTS_MANAGED = Gauge(
"ingestor_slots_managed_current", 'ingestor_slots_managed_current',
"Number of slots currently managed by this ingestor instance", 'Number of slots currently managed by this ingestor instance',
POD_ID_LABEL, POD_ID_LABEL,
) )
SLOTS_ACQUIRED = Counter( SLOTS_ACQUIRED = Counter(
"ingestor_slots_acquired_total", 'ingestor_slots_acquired_total',
"Total number of slots acquired by this instance", 'Total number of slots acquired by this instance',
POD_ID_LABEL, POD_ID_LABEL,
) )
SLOTS_RELEASED = Counter( SLOTS_RELEASED = Counter(
"ingestor_slots_released_total", 'ingestor_slots_released_total',
"Total number of slots released by this instance", 'Total number of slots released by this instance',
POD_ID_LABEL, POD_ID_LABEL,
) )
OPC_MANAGERS_ACTIVE = Gauge( OPC_MANAGERS_ACTIVE = Gauge(
"ingestor_opc_managers_active", 'ingestor_opc_managers_active',
"Number of active OPC Managers in this instance", 'Number of active OPC Managers in this instance',
POD_ID_LABEL, POD_ID_LABEL,
) )
OPC_SUBSCRIPTION_ERRORS = Counter( OPC_SUBSCRIPTION_ERRORS = Counter(
"ingestor_opc_subscription_errors_total", 'ingestor_opc_subscription_errors_total',
"Errors when trying to subscribe to OPC tags", 'Errors when trying to subscribe to OPC tags',
["pod_id", "server", "slot"], ['pod_id', 'server', 'slot'],
) )
# --- OPC Manager Metrics --- # --- OPC Manager Metrics ---
OPC_CONNECTIONS_TOTAL = Counter( OPC_CONNECTIONS_TOTAL = Counter(
"opc_connections_initiated_total", 'opc_connections_initiated_total',
"Total connection attempts to OPC servers", 'Total connection attempts to OPC servers',
["pod_id", "server_name"], ['pod_id', 'server_name'],
) )
OPC_CONNECTIONS_FAILED = Counter( OPC_CONNECTIONS_FAILED = Counter(
"opc_connections_failed_total", 'opc_connections_failed_total',
"Total failed connection attempts to OPC servers", 'Total failed connection attempts to OPC servers',
["pod_id", "server_name"], ['pod_id', 'server_name'],
) )
OPC_CONNECTION_STATUS = Gauge( OPC_CONNECTION_STATUS = Gauge(
"opc_connection_status", 'opc_connection_status',
"Connection status with the OPC server (1=connected, 0=disconnected)", 'Connection status with the OPC server (1=connected, 0=disconnected)',
SERVER_LABELS, SERVER_LABELS,
) )
OPC_SUBSCRIPTIONS_CREATED = Counter( OPC_SUBSCRIPTIONS_CREATED = Counter(
"opc_subscriptions_created_total", 'opc_subscriptions_created_total',
"Total OPC subscriptions created", 'Total OPC subscriptions created',
["pod_id", "server_name", "slot_name"], ['pod_id', 'server_name', 'slot_name'],
) )
OPC_TAGS_SUBSCRIBED = Gauge( OPC_TAGS_SUBSCRIBED = Gauge(
"opc_tags_subscribed_current", 'opc_tags_subscribed_current',
"Current number of OPC tags subscribed on a server", 'Current number of OPC tags subscribed on a server',
["pod_id", "server_name"], ['pod_id', 'server_name'],
) )
OPC_CYCLES_WITHOUT_DATA = Gauge( OPC_CYCLES_WITHOUT_DATA = Gauge(
"opc_cycles_without_data", 'opc_cycles_without_data',
"Current number of cycles without receiving data from a server", 'Current number of cycles without receiving data from a server',
["pod_id", "server_name"], ['pod_id', 'server_name'],
) )
OPC_RECONNECTIONS_TOTAL = Counter( OPC_RECONNECTIONS_TOTAL = Counter(
"opc_reconnections_tried_total", 'opc_reconnections_tried_total',
"Reconnection attempts to an OPC server after a loss", 'Reconnection attempts to an OPC server after a loss',
["pod_id", "server_name"], ['pod_id', 'server_name'],
) )
# --- Data Manager (Kafka) Metrics --- # --- Data Manager (Kafka) Metrics ---
KAFKA_MESSAGES_SENT = Counter( KAFKA_MESSAGES_SENT = Counter(
"kafka_messages_sent_total", "Total messages sent to Kafka", KAFKA_LABELS 'kafka_messages_sent_total', 'Total messages sent to Kafka', KAFKA_LABELS
) )
KAFKA_MESSAGES_ERRORS = Counter( KAFKA_MESSAGES_ERRORS = Counter(
"kafka_messages_errors_total", 'kafka_messages_errors_total',
"Total errors sending messages to Kafka", 'Total errors sending messages to Kafka',
KAFKA_LABELS, KAFKA_LABELS,
) )
KAFKA_CONNECTION_STATUS = Gauge( KAFKA_CONNECTION_STATUS = Gauge(
"kafka_connection_status", 'kafka_connection_status',
"Connection status with Kafka (1=connected, 0=disconnected)", 'Connection status with Kafka (1=connected, 0=disconnected)',
POD_ID_LABEL, POD_ID_LABEL,
) )
# --- Resource Manager (Redis) Metrics --- # --- Resource Manager (Redis) Metrics ---
REDIS_OPERATIONS_TOTAL = Counter( REDIS_OPERATIONS_TOTAL = Counter(
"redis_operations_total", "Total number of Redis operations performed", REDIS_LABELS 'redis_operations_total', 'Total number of Redis operations performed', REDIS_LABELS
) )
REDIS_OPERATIONS_ERRORS = Counter( REDIS_OPERATIONS_ERRORS = Counter(
"redis_operations_errors_total", 'redis_operations_errors_total',
"Total number of errors in Redis operations", 'Total number of errors in Redis operations',
REDIS_LABELS, REDIS_LABELS,
) )
REDIS_OPERATIONS_DURATION = Histogram( REDIS_OPERATIONS_DURATION = Histogram(
"redis_operations_duration_seconds", 'redis_operations_duration_seconds',
"Duration of Redis operations in seconds", 'Duration of Redis operations in seconds',
REDIS_LABELS, REDIS_LABELS,
) )
REDIS_CONNECTION_STATUS = Gauge( REDIS_CONNECTION_STATUS = Gauge(
"redis_connection_status", 'redis_connection_status',
"Connection status with Redis (1=connected, 0=disconnected)", 'Connection status with Redis (1=connected, 0=disconnected)',
POD_ID_LABEL, POD_ID_LABEL,
) )
# --- Notification Metrics --- # --- Notification Metrics ---
NOTIFICATIONS_SENT = Counter( NOTIFICATIONS_SENT = Counter(
"notifications_sent_total", 'notifications_sent_total',
"Total number of notifications sent", 'Total number of notifications sent',
NOTIFICATION_LABELS, NOTIFICATION_LABELS,
) )

159
pyproject.toml Normal file
View File

@@ -0,0 +1,159 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "ingestor"
version = "0.0.0"
description = "Sientia DataOps Ingestor - OPC Tag Ingestor"
readme = "README.md"
requires-python = ">=3.11"
authors = [
{name = "Aignosi", email = "dev@aignosi.com"}
]
[tool.ruff]
line-length = 100
target-version = "py311"
exclude = [
".git",
".venv",
"venv",
"__pycache__",
"*.pyc",
".pytest_cache",
"htmlcov",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"N", # pep8-naming
"YTT", # flake8-2020
"S", # flake8-bandit
"BLE", # flake8-blind-except
"A", # flake8-builtins
"C90", # mccabe complexity
]
ignore = [
"BLE001", # ignore blind except, we need to send notifications with any error
"E501", # line too long (handled by formatter)
"S101", # use of assert (needed for tests)
"S105", # possible hardcoded password (false positives)
"S106", # possible hardcoded password (false positives)
"N802", # function name should be lowercase (temporal decorators)
"N806", # variable in function should be lowercase
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # assert allowed in tests
"S105", # hardcoded passwords ok in tests
"S106", # hardcoded passwords ok in tests
]
[tool.ruff.lint.mccabe]
max-complexity = 15
[tool.ruff.format]
quote-style = "single"
indent-style = "space"
line-ending = "auto"
[tool.mypy]
python_version = "3.11"
warn_return_any = false
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = false
warn_no_return = true
strict_equality = true
ignore_missing_imports = true
# Ignore missing imports for external packages
[[tool.mypy.overrides]]
module = "temporalio.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "sientia_do.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "mlflow.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "prometheus_client.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "sientia.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "pandas.*"
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--cov=model_manager",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-report=xml",
]
markers = [
"asyncio: marks tests as async",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
[tool.coverage.run]
source = ["model_manager"]
omit = [
"*/tests/*",
"*/venv/*",
"*/__pycache__/*",
"*/site-packages/*",
]
branch = true
[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
[tool.coverage.html]
directory = "htmlcov"
[tool.bandit]
exclude_dirs = ["tests", "venv", ".venv"]
skips = ["B101", "B601"] # Skip assert and shell injection in controlled environments

19
requirements-dev.txt Normal file
View File

@@ -0,0 +1,19 @@
# Development and Testing Dependencies
# These packages are only needed for development, testing, and code quality checks
# Install with: pip install -r requirements-dev.txt
# Code Quality & Linting
ruff>=0.1.0 # Fast Python linter and formatter (replaces flake8, black, isort)
mypy>=1.7.0 # Static type checker
bandit>=1.7.5 # Security vulnerability scanner
pandas-stubs>=2.0.0 # Type stubs for pandas
types-requests>=2.31.0 # Type stubs for requests
# Testing
pytest>=7.4.0 # Testing framework
pytest-cov>=4.1.0 # Coverage plugin for pytest
pytest-asyncio>=0.21.0 # Async test support (already in main requirements)
# Development Tools
ipython>=8.12.0 # Enhanced Python shell
ipdb>=0.13.13 # IPython debugger

View File

@@ -1,5 +1,5 @@
asyncua==1.1.5 asyncua==1.1.5
redis redis
git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.4.3 git+ssh://git@github.com/Aignosi/sientia-dataops-library.git@1.4.6
prometheus_client prometheus_client
pymongo pymongo

View File

@@ -1,33 +1,34 @@
from unittest.mock import ANY, MagicMock, patch from unittest.mock import ANY, MagicMock, patch
from pytest import fixture
from kafka.errors import NoBrokersAvailable from kafka.errors import NoBrokersAvailable
from pytest import fixture
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from ingestor.managers.data_manager import DataManager from ingestor.managers.data_manager import DataManager
metadata = { metadata = {
"metadata": { 'metadata': {
"model_id": "test_model", 'model_id': 'test_model',
"model_name": "test_model", 'model_name': 'test_model',
"workflow_name": "test_workflow", 'workflow_name': 'test_workflow',
"schema_name": "test_schedule", 'schema_name': 'test_schedule',
"pod_id": "localhost", 'pod_id': 'localhost',
}, },
} }
@fixture @fixture
@patch("ingestor.managers.data_manager.KafkaProducer") @patch('ingestor.managers.data_manager.KafkaProducer')
@patch("ingestor.managers.data_manager.MongoClient") @patch('ingestor.managers.data_manager.MongoClient')
def data_manager(mongo, kafka): def data_manager(mongo, kafka):
data_manager = DataManager( data_manager = DataManager(
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=True, export_to_kafka=True,
logger=MagicMock(), logger=MagicMock(),
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
data_manager.send_notification = MagicMock() data_manager.send_notification = MagicMock()
@@ -35,112 +36,106 @@ def data_manager(mongo, kafka):
return data_manager return data_manager
@patch("ingestor.managers.data_manager.KafkaProducer") @patch('ingestor.managers.data_manager.KafkaProducer')
@patch("ingestor.managers.data_manager.MongoClient") @patch('ingestor.managers.data_manager.MongoClient')
def test___init___success(mongo, kafka): def test___init___success(mongo, kafka):
logger_mock = MagicMock() logger_mock = MagicMock()
data_manager = DataManager( data_manager = DataManager(
metadata=metadata["metadata"], metadata=metadata['metadata'],
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=True, export_to_kafka=True,
logger=logger_mock, logger=logger_mock,
notification_handler=MagicMock() notification_handler=MagicMock(),
) )
kafka.assert_called_once_with( kafka.assert_called_once_with(
bootstrap_servers="localhost:9092", bootstrap_servers='localhost:9092', value_serializer=ANY, key_serializer=ANY
value_serializer=ANY,
key_serializer=ANY
) )
assert data_manager.kafka_producer is not None assert data_manager.kafka_producer is not None
logger_mock.info.assert_any_call( logger_mock.info.assert_any_call(
"Trying (0) to initializing DataManager with Kafka servers: localhost:9092" 'Trying (0) to initializing DataManager with Kafka servers: localhost:9092'
)
logger_mock.info.assert_any_call(
"DataManager initialized with Kafka servers: localhost:9092"
) )
logger_mock.info.assert_any_call('DataManager initialized with Kafka servers: localhost:9092')
logger_mock.error.assert_not_called() logger_mock.error.assert_not_called()
assert logger_mock.info.call_count == 4 assert logger_mock.info.call_count == 4
@patch("ingestor.managers.data_manager.KafkaProducer") @patch('ingestor.managers.data_manager.KafkaProducer')
@patch("ingestor.managers.data_manager.MongoClient") @patch('ingestor.managers.data_manager.MongoClient')
def test___init___second_attempt(mongo, kafka): def test___init___second_attempt(mongo, kafka):
kafka.side_effect = [NoBrokersAvailable, MagicMock()] kafka.side_effect = [NoBrokersAvailable, MagicMock()]
logger_mock = MagicMock() logger_mock = MagicMock()
data_manager = DataManager( data_manager = DataManager(
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=True, export_to_kafka=True,
logger=logger_mock, logger=logger_mock,
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
kafka.assert_any_call( kafka.assert_any_call(
bootstrap_servers="localhost:9092", bootstrap_servers='localhost:9092', value_serializer=ANY, key_serializer=ANY
value_serializer=ANY,
key_serializer=ANY
) )
assert kafka.call_count == 2 assert kafka.call_count == 2
assert data_manager.kafka_producer is not None assert data_manager.kafka_producer is not None
logger_mock.info.assert_any_call( logger_mock.info.assert_any_call(
"Trying (0) to initializing DataManager with Kafka servers: localhost:9092" 'Trying (0) to initializing DataManager with Kafka servers: localhost:9092'
) )
logger_mock.info.assert_any_call( logger_mock.info.assert_any_call(
"Trying (1) to initializing DataManager with Kafka servers: localhost:9092" 'Trying (1) to initializing DataManager with Kafka servers: localhost:9092'
)
logger_mock.info.assert_any_call(
"DataManager initialized with Kafka servers: localhost:9092"
) )
logger_mock.info.assert_any_call('DataManager initialized with Kafka servers: localhost:9092')
logger_mock.error.assert_called_once_with( logger_mock.error.assert_called_once_with(
"Kafka servers localhost:9092 are not available. Retrying..." 'Kafka servers localhost:9092 are not available. Retrying...'
) )
assert logger_mock.info.call_count == 5 assert logger_mock.info.call_count == 5
@patch("ingestor.managers.data_manager.KafkaProducer") @patch('ingestor.managers.data_manager.KafkaProducer')
@patch("ingestor.managers.data_manager.MongoClient") @patch('ingestor.managers.data_manager.MongoClient')
def test___init___failure_max_attempts(mongo, kafka): def test___init___failure_max_attempts(mongo, kafka):
kafka.side_effect = NoBrokersAvailable kafka.side_effect = NoBrokersAvailable
logger_mock = MagicMock() logger_mock = MagicMock()
try: try:
DataManager( DataManager(
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=True, export_to_kafka=True,
logger=logger_mock, logger=logger_mock,
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
except NoBrokersAvailable as e: except NoBrokersAvailable as e:
assert str( assert (
e) == "NoBrokersAvailable: Failed to connect to Kafka servers localhost:9092 after 3 attempts." str(e)
== 'NoBrokersAvailable: Failed to connect to Kafka servers localhost:9092 after 3 attempts.'
)
assert kafka.call_count == 3 assert kafka.call_count == 3
logger_mock.info.assert_any_call( logger_mock.info.assert_any_call(
"Trying (0) to initializing DataManager with Kafka servers: localhost:9092" 'Trying (0) to initializing DataManager with Kafka servers: localhost:9092'
) )
logger_mock.info.assert_any_call( logger_mock.info.assert_any_call(
"Trying (1) to initializing DataManager with Kafka servers: localhost:9092" 'Trying (1) to initializing DataManager with Kafka servers: localhost:9092'
) )
logger_mock.info.assert_any_call( logger_mock.info.assert_any_call(
"Trying (2) to initializing DataManager with Kafka servers: localhost:9092" 'Trying (2) to initializing DataManager with Kafka servers: localhost:9092'
) )
logger_mock.error.assert_called_with( logger_mock.error.assert_called_with(
"Failed to connect to Kafka servers localhost:9092 after 3 attempts." 'Failed to connect to Kafka servers localhost:9092 after 3 attempts.'
) )
assert logger_mock.info.call_count == 3 assert logger_mock.info.call_count == 3
else: else:
assert False, "Expected NoBrokersAvailable exception was not raised." raise AssertionError('Expected NoBrokersAvailable exception was not raised.')
def test_shutdown_has_producer(data_manager): def test_shutdown_has_producer(data_manager):
@@ -161,7 +156,7 @@ def test_shutdown_no_producer(data_manager):
data_manager.shutdown() data_manager.shutdown()
data_manager.logger.warning.assert_any_call( data_manager.logger.warning.assert_any_call(
"Kafka producer is already closed or not initialized." 'Kafka producer is already closed or not initialized.'
) )
@@ -171,30 +166,24 @@ def test_shutdown_no_mongo_client(data_manager):
data_manager.shutdown() data_manager.shutdown()
data_manager.logger.warning.assert_any_call( data_manager.logger.warning.assert_any_call(
"MongoDB client is already closed or not initialized." 'MongoDB client is already closed or not initialized.'
) )
def test_shutdown_exception(data_manager): def test_shutdown_exception(data_manager):
data_manager.kafka_producer.flush = MagicMock( data_manager.kafka_producer.flush = MagicMock(side_effect=Exception('Test error'))
side_effect=Exception("Test error"))
data_manager.kafka_producer.close = MagicMock() data_manager.kafka_producer.close = MagicMock()
data_manager.shutdown() data_manager.shutdown()
data_manager.logger.error.assert_called_once_with( data_manager.logger.error.assert_called_once_with('Error closing Kafka producer: Test error')
"Error closing Kafka producer: Test error"
)
def test_shutdown_exception_mongo(data_manager): def test_shutdown_exception_mongo(data_manager):
data_manager.mongo_client.close = MagicMock( data_manager.mongo_client.close = MagicMock(side_effect=Exception('Test error'))
side_effect=Exception("Test error"))
data_manager.shutdown() data_manager.shutdown()
data_manager.logger.error.assert_called_once_with( data_manager.logger.error.assert_called_once_with('Error closing MongoDB client: Test error')
"Error closing MongoDB client: Test error"
)
def test___del__(data_manager): def test___del__(data_manager):
@@ -205,29 +194,27 @@ def test___del__(data_manager):
def test_delivery_report(data_manager): def test_delivery_report(data_manager):
msg = MagicMock() msg = MagicMock()
msg.topic = "test_topic" msg.topic = 'test_topic'
msg.partition = 0 msg.partition = 0
msg.offset = 1 msg.offset = 1
data_manager.delivery_report(msg) data_manager.delivery_report(msg)
data_manager.logger.debug.assert_called_once_with( data_manager.logger.debug.assert_called_once_with(
f"Record successfully produced to {msg.topic} [{msg.partition}] at offset {msg.offset}" f'Record successfully produced to {msg.topic} [{msg.partition}] at offset {msg.offset}'
) )
def test_delivery_error(data_manager): def test_delivery_error(data_manager):
err = "Test error" err = 'Test error'
data_manager.delivery_error(err) data_manager.delivery_error(err)
data_manager.logger.error.assert_called_once_with( data_manager.logger.error.assert_called_once_with(f'Delivery failed for record : {err}')
f"Delivery failed for record : {err}"
)
def test_publish(data_manager): def test_publish(data_manager):
topic = "test_topic" topic = 'test_topic'
data = {"key": "value"} data = {'key': 'value'}
# Mock the send method of the Kafka producer # Mock the send method of the Kafka producer
send_mock = MagicMock() send_mock = MagicMock()
@@ -237,9 +224,7 @@ def test_publish(data_manager):
data_manager.publish(topic, data) data_manager.publish(topic, data)
# Check if the send method was called with the correct arguments # Check if the send method was called with the correct arguments
send_mock.assert_called_once_with( send_mock.assert_called_once_with(topic=topic, value=data)
topic=topic, value=data
)
send_mock.return_value.add_callback.assert_called_once() send_mock.return_value.add_callback.assert_called_once()
@@ -248,57 +233,53 @@ def test_publish(data_manager):
def test_publish_no_kafka(data_manager): def test_publish_no_kafka(data_manager):
data_manager.export_to_kafka = False data_manager.export_to_kafka = False
topic = "test_topic" topic = 'test_topic'
data = {"key": "value"} data = {'key': 'value'}
data_manager.publish(topic, data) data_manager.publish(topic, data)
data_manager.kafka_producer.send.assert_not_called() data_manager.kafka_producer.send.assert_not_called()
@patch("ingestor.managers.data_manager.traceback") @patch('ingestor.managers.data_manager.traceback')
def test_publish_error(traceback, data_manager): def test_publish_error(traceback, data_manager):
topic = "test_topic" topic = 'test_topic'
data = { data = {'key': 'value', 'name': 'test_tag'}
"key": "value",
"name": "test_tag"
}
# Mock the send method of the Kafka producer to raise an exception # Mock the send method of the Kafka producer to raise an exception
send_mock = MagicMock(side_effect=Exception("Test error")) send_mock = MagicMock(side_effect=Exception('Test error'))
data_manager.kafka_producer.send = send_mock data_manager.kafka_producer.send = send_mock
# Call the publish method # Call the publish method
data_manager.publish(topic, data) data_manager.publish(topic, data)
# Check if the send method was called with the correct arguments # Check if the send method was called with the correct arguments
send_mock.assert_called_once_with( send_mock.assert_called_once_with(topic=topic, value=data)
topic=topic, value=data
)
# Check if the error was logged # Check if the error was logged
data_manager.send_notification.assert_called_once_with( data_manager.send_notification.assert_called_once_with(
notification_id=f"KAFKA_PRODUCER_ERROR_{topic}", notification_id=f'KAFKA_PRODUCER_ERROR_{topic}',
message=f"Error publishing message to topic {topic}: Test error", message=f'Error publishing message to topic {topic}: Test error',
block="kafka_producer", block='kafka_producer',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=traceback.format_exc.return_value, attachment_content=traceback.format_exc.return_value,
metadata=metadata["metadata"] metadata=metadata['metadata'],
) )
def test_publish_error_mongo(data_manager): def test_publish_error_mongo(data_manager):
data_manager.export_to_kafka = False data_manager.export_to_kafka = False
data_manager.mongo_db.__getitem__.return_value.insert_one = MagicMock( data_manager.mongo_db.__getitem__.return_value.insert_one = MagicMock(
side_effect=Exception("Test error")) side_effect=Exception('Test error')
)
data_manager.publish("test_topic", {"key": "value"}) data_manager.publish('test_topic', {'key': 'value'})
data_manager.send_notification.assert_called_once_with( data_manager.send_notification.assert_called_once_with(
notification_id="MONGO_PRODUCER_ERROR_test_topic", notification_id='MONGO_PRODUCER_ERROR_test_topic',
message="Error inserting message to MongoDB: Test error", message='Error inserting message to MongoDB: Test error',
block="mongo_producer", block='mongo_producer',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=ANY, attachment_content=ANY,
metadata=metadata["metadata"] metadata=metadata['metadata'],
) )

View File

@@ -1,14 +1,16 @@
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from pytest import fixture, mark from pytest import fixture, mark
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from ingestor.managers.ingestor_manager import IngestorManager from ingestor.managers.ingestor_manager import IngestorManager
metadata = { metadata = {
"metadata": { 'metadata': {
"model_id": "test_model", 'model_id': 'test_model',
"model_name": "test_model", 'model_name': 'test_model',
"workflow_name": "test_workflow", 'workflow_name': 'test_workflow',
"schema_name": "test_schedule", 'schema_name': 'test_schedule',
}, },
} }
@@ -18,20 +20,17 @@ metadata = {
@patch('ingestor.managers.ingestor_manager.ResourceManager') @patch('ingestor.managers.ingestor_manager.ResourceManager')
def ingestor_manager(data_manager_mock, resource_manager_mock): def ingestor_manager(data_manager_mock, resource_manager_mock):
ingestor = IngestorManager( ingestor = IngestorManager(
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
redis_data={ redis_data={'host': 'localhost', 'port': 6379},
"host": "localhost",
"port": 6379
},
lease_ttl=60, lease_ttl=60,
heartbeat_ttl=60, heartbeat_ttl=60,
poll_interval=5, poll_interval=5,
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=False, export_to_kafka=False,
logger=MagicMock(), logger=MagicMock(),
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
ingestor.send_notification = MagicMock() ingestor.send_notification = MagicMock()
@@ -43,41 +42,39 @@ def ingestor_manager(data_manager_mock, resource_manager_mock):
@patch('ingestor.managers.ingestor_manager.DataManager') @patch('ingestor.managers.ingestor_manager.DataManager')
@patch('ingestor.managers.ingestor_manager.ResourceManager') @patch('ingestor.managers.ingestor_manager.ResourceManager')
@patch('ingestor.managers.ingestor_manager.NotificationHandler') @patch('ingestor.managers.ingestor_manager.NotificationHandler')
def test___init__(notification_handler_mock, resource_manager_mock, data_manager_mock, opc_manager_mock): def test___init__(
notification_handler_mock, resource_manager_mock, data_manager_mock, opc_manager_mock
):
ingestor = IngestorManager( ingestor = IngestorManager(
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
redis_data={ redis_data={'host': 'localhost', 'port': 6379},
"host": "localhost",
"port": 6379
},
lease_ttl=60, lease_ttl=60,
heartbeat_ttl=60, heartbeat_ttl=60,
poll_interval=5, poll_interval=5,
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=False, export_to_kafka=False,
logger=MagicMock(), logger=MagicMock(),
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
opc_manager_mock.assert_not_called() opc_manager_mock.assert_not_called()
data_manager_mock.assert_called_once_with( data_manager_mock.assert_called_once_with(
kafka_servers="localhost:9092", kafka_servers='localhost:9092',
mongo_connection_string="mongodb://localhost:27017", mongo_connection_string='mongodb://localhost:27017',
mongo_database="sientia", mongo_database='sientia',
export_to_kafka=False, export_to_kafka=False,
metadata=metadata["metadata"], metadata=metadata['metadata'],
logger=ingestor.logger, logger=ingestor.logger,
notification_handler=ingestor.notification_handler, notification_handler=ingestor.notification_handler,
) )
resource_manager_mock.assert_called_once_with( resource_manager_mock.assert_called_once_with(
host="localhost", host='localhost',
port=6379, port=6379,
lease_ttl=60, lease_ttl=60,
heartbeat_ttl=60, heartbeat_ttl=60,
metadata=metadata["metadata"], metadata=metadata['metadata'],
logger=ingestor.logger, logger=ingestor.logger,
notification_handler=ingestor.notification_handler, notification_handler=ingestor.notification_handler,
username=None, username=None,
@@ -101,12 +98,11 @@ async def test_initialize_opc_from_config(opc_manager, ingestor_manager):
'cert_path': '/path/to/cert', 'cert_path': '/path/to/cert',
'private_key_path': '/path/to/private_key', 'private_key_path': '/path/to/private_key',
'server_cert_path': '/path/to/server_cert', 'server_cert_path': '/path/to/server_cert',
'pod_id': 'test_pod' 'pod_id': 'test_pod',
} }
opc_manager.return_value = MagicMock(connect=AsyncMock()) opc_manager.return_value = MagicMock(connect=AsyncMock())
result = await ingestor_manager.initialize_opc_from_config( result = await ingestor_manager.initialize_opc_from_config(server_config)
server_config)
opc_manager.assert_called_once_with( opc_manager.assert_called_once_with(
name=server_config['name'], name=server_config['name'],
@@ -118,7 +114,7 @@ async def test_initialize_opc_from_config(opc_manager, ingestor_manager):
cert_path=server_config['cert_path'], cert_path=server_config['cert_path'],
private_key_path=server_config['private_key_path'], private_key_path=server_config['private_key_path'],
server_cert_path=server_config['server_cert_path'], server_cert_path=server_config['server_cert_path'],
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
assert result == opc_manager.return_value assert result == opc_manager.return_value
@@ -135,25 +131,24 @@ async def test_initialize_opc_from_config_exception(traceback_mock, opc_manager,
'server_uri': 'http://opcua-server.simulator', 'server_uri': 'http://opcua-server.simulator',
'cert_path': '/path/to/cert', 'cert_path': '/path/to/cert',
'private_key_path': '/path/to/private_key', 'private_key_path': '/path/to/private_key',
'server_cert_path': '/path/to/server_cert' 'server_cert_path': '/path/to/server_cert',
} }
ingestor_manager.logger.error = MagicMock() ingestor_manager.logger.error = MagicMock()
opc_manager.side_effect = Exception("Initialization error") opc_manager.side_effect = Exception('Initialization error')
result = await ingestor_manager.initialize_opc_from_config( result = await ingestor_manager.initialize_opc_from_config(server_config)
server_config)
assert result is None assert result is None
traceback_mock.format_exc.assert_called_once() traceback_mock.format_exc.assert_called_once()
ingestor_manager.send_notification.assert_called_once_with( ingestor_manager.send_notification.assert_called_once_with(
metadata=metadata["metadata"], metadata=metadata['metadata'],
notification_id=f'OPC_CONNECTION_ERROR_{server_config["name"]}', notification_id=f'OPC_CONNECTION_ERROR_{server_config["name"]}',
message='Error initializing OPC manager: Initialization error', message='Error initializing OPC manager: Initialization error',
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=traceback_mock.format_exc.return_value attachment_content=traceback_mock.format_exc.return_value,
) )
@@ -161,46 +156,33 @@ async def test_initialize_opc_from_config_exception(traceback_mock, opc_manager,
@patch('ingestor.managers.ingestor_manager.OpcManager') @patch('ingestor.managers.ingestor_manager.OpcManager')
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
async def test_update_opc_servers(metrics, opc_manager, ingestor_manager): async def test_update_opc_servers(metrics, opc_manager, ingestor_manager):
manager1 = MagicMock( manager1 = MagicMock(config={'config': 'config1'})
config={"config": "config1"} manager2 = MagicMock(config={'config': 'config2'})
) manager3 = MagicMock(config={'config': 'config3'})
manager2 = MagicMock(
config={"config": "config2"}
)
manager3 = MagicMock(
config={"config": "config3"}
)
async def mock_initialize_from_config(config): async def mock_initialize_from_config(config):
if config == {"config": "config1"}: if config == {'config': 'config1'}:
return manager1 return manager1
elif config == {"config": "config2"}: elif config == {'config': 'config2'}:
return manager2 return manager2
elif config == {"config": "config3"}: elif config == {'config': 'config3'}:
return manager3 return manager3
else: else:
return None return None
ingestor_manager.initialize_opc_from_config = AsyncMock( ingestor_manager.initialize_opc_from_config = AsyncMock(side_effect=mock_initialize_from_config)
side_effect=mock_initialize_from_config
)
ingestor_manager.managed_tags = { ingestor_manager.managed_tags = {
"slot1": { 'slot1': {
"server1": {"config": "config1"}, 'server1': {'config': 'config1'},
"server2": {"config": "config2"}, 'server2': {'config': 'config2'},
'server5': {"config": "config5"} 'server5': {'config': 'config5'},
}, },
"slot2": { 'slot2': {'server3': {'config': 'config3'}, 'server1': {'config': 'config1'}},
"server3": {"config": "config3"},
"server1": {"config": "config1"}
}
} }
mock = MagicMock( mock = MagicMock(config={'config': 'old_config2'})
config={"config": "old_config2"}) ingestor_manager.opc_managers['server3'] = AsyncMock(config={'config': 'config3'})
ingestor_manager.opc_managers['server3'] = AsyncMock(
config={"config": "config3"})
ingestor_manager.opc_managers['server2'] = mock ingestor_manager.opc_managers['server2'] = mock
ingestor_manager.opc_managers['server4'] = AsyncMock() ingestor_manager.opc_managers['server4'] = AsyncMock()
@@ -208,29 +190,23 @@ async def test_update_opc_servers(metrics, opc_manager, ingestor_manager):
assert len(ingestor_manager.opc_managers) == 3 assert len(ingestor_manager.opc_managers) == 3
ingestor_manager.initialize_opc_from_config.assert_any_call( ingestor_manager.initialize_opc_from_config.assert_any_call({'config': 'config1'})
{"config": "config1"}) ingestor_manager.initialize_opc_from_config.assert_any_call({'config': 'config2'})
ingestor_manager.initialize_opc_from_config.assert_any_call( ingestor_manager.initialize_opc_from_config.assert_any_call({'config': 'config5'})
{"config": "config2"})
ingestor_manager.initialize_opc_from_config.assert_any_call(
{"config": "config5"})
assert ingestor_manager.initialize_opc_from_config.call_count == 3 assert ingestor_manager.initialize_opc_from_config.call_count == 3
assert ingestor_manager.opc_managers['server1'].config == { assert ingestor_manager.opc_managers['server1'].config == {'config': 'config1'}
"config": "config1"} assert ingestor_manager.opc_managers['server2'].config == {'config': 'config2'}
assert ingestor_manager.opc_managers['server2'].config == { assert ingestor_manager.opc_managers['server3'].config == {'config': 'config3'}
"config": "config2"}
assert ingestor_manager.opc_managers['server3'].config == {
"config": "config3"}
assert 'server4' not in ingestor_manager.opc_managers assert 'server4' not in ingestor_manager.opc_managers
assert 'server5' not in ingestor_manager.opc_managers assert 'server5' not in ingestor_manager.opc_managers
assert ingestor_manager.opc_managers['server2'] != mock assert ingestor_manager.opc_managers['server2'] != mock
metrics.OPC_MANAGERS_ACTIVE.labels.assert_called_once_with( metrics.OPC_MANAGERS_ACTIVE.labels.assert_called_once_with(pod_id=ingestor_manager.pod_id)
pod_id=ingestor_manager.pod_id)
metrics.OPC_MANAGERS_ACTIVE.labels.return_value.set.assert_called_once_with( metrics.OPC_MANAGERS_ACTIVE.labels.return_value.set.assert_called_once_with(
len(ingestor_manager.opc_managers)) len(ingestor_manager.opc_managers)
)
def test_declare_active(ingestor_manager): def test_declare_active(ingestor_manager):
@@ -246,8 +222,7 @@ def test_get_active_ingestors(ingestor_manager):
def test_get_active_ingestors_empty(ingestor_manager): def test_get_active_ingestors_empty(ingestor_manager):
ingestor_manager.resource_manager.get_all_ingestors = MagicMock( ingestor_manager.resource_manager.get_all_ingestors = MagicMock(return_value=None)
return_value=None)
result = ingestor_manager.get_active_ingestors() result = ingestor_manager.get_active_ingestors()
assert result == [] assert result == []
ingestor_manager.resource_manager.get_all_ingestors.assert_called_once() ingestor_manager.resource_manager.get_all_ingestors.assert_called_once()
@@ -255,8 +230,7 @@ def test_get_active_ingestors_empty(ingestor_manager):
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
def test_get_number_of_leases_success(metrics, ingestor_manager): def test_get_number_of_leases_success(metrics, ingestor_manager):
ingestor_manager.resource_manager.get_all_leases = MagicMock( ingestor_manager.resource_manager.get_all_leases = MagicMock(return_value=['lease1', 'lease2'])
return_value=["lease1", "lease2"])
result = ingestor_manager.get_number_of_leases() result = ingestor_manager.get_number_of_leases()
assert result == 2 assert result == 2
ingestor_manager.resource_manager.get_all_leases.assert_called_once() ingestor_manager.resource_manager.get_all_leases.assert_called_once()
@@ -265,8 +239,7 @@ def test_get_number_of_leases_success(metrics, ingestor_manager):
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
def test_get_number_of_leases_empty(metrics, ingestor_manager): def test_get_number_of_leases_empty(metrics, ingestor_manager):
ingestor_manager.resource_manager.get_all_leases = MagicMock( ingestor_manager.resource_manager.get_all_leases = MagicMock(return_value=None)
return_value=None)
result = ingestor_manager.get_number_of_leases() result = ingestor_manager.get_number_of_leases()
assert result == 0 assert result == 0
ingestor_manager.resource_manager.get_all_leases.assert_called_once() ingestor_manager.resource_manager.get_all_leases.assert_called_once()
@@ -275,8 +248,7 @@ def test_get_number_of_leases_empty(metrics, ingestor_manager):
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
def test_get_number_of_slots_success(metrics, ingestor_manager): def test_get_number_of_slots_success(metrics, ingestor_manager):
ingestor_manager.resource_manager.get_all_slots = MagicMock( ingestor_manager.resource_manager.get_all_slots = MagicMock(return_value=['slot1', 'slot2'])
return_value=["slot1", "slot2"])
result = ingestor_manager.get_number_of_slots() result = ingestor_manager.get_number_of_slots()
assert result == 2 assert result == 2
ingestor_manager.resource_manager.get_all_slots.assert_called_once() ingestor_manager.resource_manager.get_all_slots.assert_called_once()
@@ -285,8 +257,7 @@ def test_get_number_of_slots_success(metrics, ingestor_manager):
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
def test_get_number_of_slots_empty(metrics, ingestor_manager): def test_get_number_of_slots_empty(metrics, ingestor_manager):
ingestor_manager.resource_manager.get_all_slots = MagicMock( ingestor_manager.resource_manager.get_all_slots = MagicMock(return_value=None)
return_value=None)
result = ingestor_manager.get_number_of_slots() result = ingestor_manager.get_number_of_slots()
assert result == 0 assert result == 0
ingestor_manager.resource_manager.get_all_slots.assert_called_once() ingestor_manager.resource_manager.get_all_slots.assert_called_once()
@@ -294,39 +265,32 @@ def test_get_number_of_slots_empty(metrics, ingestor_manager):
def test_get_slot_leases_1_success(ingestor_manager): def test_get_slot_leases_1_success(ingestor_manager):
ingestor_manager.resource_manager.lease_tag = MagicMock( ingestor_manager.resource_manager.lease_tag = MagicMock(return_value=True)
return_value=True) ingestor_manager.resource_manager.get_tag_slot = MagicMock(return_value={'tags': ['tag1']})
ingestor_manager.resource_manager.get_tag_slot = MagicMock(
return_value={"tags": ["tag1"]})
ingestor_manager.number_of_slots = 1 ingestor_manager.number_of_slots = 1
result = ingestor_manager.get_slot_leases() result = ingestor_manager.get_slot_leases()
assert result == { assert result == {'1': {'tags': ['tag1']}}
"1": {"tags": ["tag1"]}
}
def test_get_slot_leases_2_success(ingestor_manager): def test_get_slot_leases_2_success(ingestor_manager):
ingestor_manager.resource_manager.lease_tag = MagicMock( ingestor_manager.resource_manager.lease_tag = MagicMock(side_effect=[True, True])
side_effect=[True, True])
ingestor_manager.resource_manager.get_tag_slot = MagicMock( ingestor_manager.resource_manager.get_tag_slot = MagicMock(
side_effect=[{"tags": ["tag1"]}, {"tags": ["tag2"]}]) side_effect=[{'tags': ['tag1']}, {'tags': ['tag2']}]
)
ingestor_manager.number_of_slots = 2 ingestor_manager.number_of_slots = 2
result = ingestor_manager.get_slot_leases(max_slots=2) result = ingestor_manager.get_slot_leases(max_slots=2)
assert result == { assert result == {'1': {'tags': ['tag1']}, '2': {'tags': ['tag2']}}
"1": {"tags": ["tag1"]},
"2": {"tags": ["tag2"]}
}
def test_get_slot_leases_2_1_none(ingestor_manager): def test_get_slot_leases_2_1_none(ingestor_manager):
ingestor_manager.resource_manager.lease_tag = MagicMock( ingestor_manager.resource_manager.lease_tag = MagicMock(side_effect=[True, True])
side_effect=[True, True])
ingestor_manager.resource_manager.get_tag_slot = MagicMock( ingestor_manager.resource_manager.get_tag_slot = MagicMock(
side_effect=[None, {"tags": ["tag1"]}]) side_effect=[None, {'tags': ['tag1']}]
)
ingestor_manager.number_of_slots = 1 ingestor_manager.number_of_slots = 1
result = ingestor_manager.get_slot_leases(max_slots=1) result = ingestor_manager.get_slot_leases(max_slots=1)
@@ -335,10 +299,8 @@ def test_get_slot_leases_2_1_none(ingestor_manager):
def test_get_slot_leases_1_failure(ingestor_manager): def test_get_slot_leases_1_failure(ingestor_manager):
ingestor_manager.resource_manager.lease_tag = MagicMock( ingestor_manager.resource_manager.lease_tag = MagicMock(return_value=False)
return_value=False) ingestor_manager.resource_manager.get_tag_slot = MagicMock(return_value={'tags': ['tag1']})
ingestor_manager.resource_manager.get_tag_slot = MagicMock(
return_value={"tags": ["tag1"]})
result = ingestor_manager.get_slot_leases() result = ingestor_manager.get_slot_leases()
ingestor_manager.resource_manager.get_tag_slot.assert_not_called() ingestor_manager.resource_manager.get_tag_slot.assert_not_called()
@@ -349,42 +311,30 @@ def test_get_slot_leases_1_failure(ingestor_manager):
@mark.asyncio @mark.asyncio
async def test_unsubscribe_slot(ingestor_manager): async def test_unsubscribe_slot(ingestor_manager):
ingestor_manager.managed_tags = { ingestor_manager.managed_tags = {
"slot1": { 'slot1': {'server1': {'tags': 'config1'}, 'server2': {'tags': 'config2'}},
"server1": {"tags": "config1"}, 'slot2': {'server3': {'tags': 'config3'}, 'server1': {'tags': 'config1'}},
"server2": {"tags": "config2"}
},
"slot2": {
"server3": {"tags": "config3"},
"server1": {"tags": "config1"}
}
} }
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {
"server1": AsyncMock(), 'server1': AsyncMock(),
"server2": AsyncMock(), 'server2': AsyncMock(),
"server3": AsyncMock() 'server3': AsyncMock(),
} }
await ingestor_manager.unsubscribe_slot("slot1") await ingestor_manager.unsubscribe_slot('slot1')
ingestor_manager.opc_managers["server1"].unsubscribe.assert_called_once_with( ingestor_manager.opc_managers['server1'].unsubscribe.assert_called_once_with('slot1')
"slot1") ingestor_manager.opc_managers['server2'].unsubscribe.assert_called_once_with('slot1')
ingestor_manager.opc_managers["server2"].unsubscribe.assert_called_once_with( ingestor_manager.opc_managers['server3'].unsubscribe.assert_not_called()
"slot1")
ingestor_manager.opc_managers["server3"].unsubscribe.assert_not_called()
def test_update_slot_config(ingestor_manager): def test_update_slot_config(ingestor_manager):
ingestor_manager.managed_tags = { ingestor_manager.managed_tags = {
"slot1": {"config": "old_config"}, 'slot1': {'config': 'old_config'},
"slot2": {"config": "new_config"}, 'slot2': {'config': 'new_config'},
"slot3": {"config": "old_config"} 'slot3': {'config': 'old_config'},
} }
ingestor_manager.resource_manager.get_tag_slot = MagicMock( ingestor_manager.resource_manager.get_tag_slot = MagicMock(
side_effect=[ side_effect=[{'config': 'updated_config'}, {'config': 'new_config'}, None]
{"config": "updated_config"},
{"config": "new_config"},
None
]
) )
ingestor_manager.update_opc_servers = MagicMock() ingestor_manager.update_opc_servers = MagicMock()
@@ -393,179 +343,130 @@ def test_update_slot_config(ingestor_manager):
ingestor_manager.update_slot_config() ingestor_manager.update_slot_config()
assert ingestor_manager.managed_tags["slot1"] == { assert ingestor_manager.managed_tags['slot1'] == {'config': 'updated_config'}
"config": "updated_config"} assert ingestor_manager.managed_tags['slot2'] == {'config': 'new_config'}
assert ingestor_manager.managed_tags["slot2"] == { assert 'slot3' not in ingestor_manager.managed_tags
"config": "new_config"}
assert "slot3" not in ingestor_manager.managed_tags
ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot1") ingestor_manager.resource_manager.renew_tag_lease.assert_any_call('slot1')
ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot3") ingestor_manager.resource_manager.renew_tag_lease.assert_any_call('slot3')
ingestor_manager.resource_manager.renew_tag_lease.assert_any_call("slot2") ingestor_manager.resource_manager.renew_tag_lease.assert_any_call('slot2')
assert ingestor_manager.resource_manager.renew_tag_lease.call_count == 3 assert ingestor_manager.resource_manager.renew_tag_lease.call_count == 3
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
def test_drop_slot_leases(metrics, ingestor_manager): def test_drop_slot_leases(metrics, ingestor_manager):
ingestor_manager.resource_manager.drop_tag_lease = MagicMock() ingestor_manager.resource_manager.drop_tag_lease = MagicMock()
ingestor_manager.drop_slot_leases(["1", "2"]) ingestor_manager.drop_slot_leases(['1', '2'])
ingestor_manager.resource_manager.drop_tag_lease.assert_any_call("1") ingestor_manager.resource_manager.drop_tag_lease.assert_any_call('1')
ingestor_manager.resource_manager.drop_tag_lease.assert_any_call("2") ingestor_manager.resource_manager.drop_tag_lease.assert_any_call('2')
metrics.SLOTS_RELEASED.labels.assert_any_call( metrics.SLOTS_RELEASED.labels.assert_any_call(pod_id=ingestor_manager.pod_id)
pod_id=ingestor_manager.pod_id)
metrics.SLOTS_RELEASED.labels.return_value.inc.assert_any_call() metrics.SLOTS_RELEASED.labels.return_value.inc.assert_any_call()
@mark.asyncio @mark.asyncio
async def test_manage_server_no_server(ingestor_manager): async def test_manage_server_no_server(ingestor_manager):
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': MagicMock(), 'server2': MagicMock()}
"server1": MagicMock(), server_config = {'tags': 'config1'}
"server2": MagicMock()
}
server_config = {
'tags': 'config1'
}
result = await ingestor_manager.manage_server( result = await ingestor_manager.manage_server('slot1', 'server3', server_config, server_config)
'slot1', 'server3', server_config, server_config)
assert result == 1 assert result == 1
ingestor_manager.opc_managers["server1"].create_subscription.assert_not_called( ingestor_manager.opc_managers['server1'].create_subscription.assert_not_called()
) ingestor_manager.opc_managers['server1'].subscribe.assert_not_called()
ingestor_manager.opc_managers["server1"].subscribe.assert_not_called()
@mark.asyncio @mark.asyncio
async def test_manage_server_create_subscription_failure(ingestor_manager): async def test_manage_server_create_subscription_failure(ingestor_manager):
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': MagicMock(), 'server2': MagicMock()}
"server1": MagicMock(), ingestor_manager.subscriptions = {'server1': MagicMock()}
"server2": MagicMock() server_config = {'tags': 'config1'}
}
ingestor_manager.subscriptions = {
"server1": MagicMock()
}
server_config = {
'tags': 'config1'
}
ingestor_manager.opc_managers["server1"].create_subscription.side_effect = Exception( ingestor_manager.opc_managers['server1'].create_subscription.side_effect = Exception(
"Subscription error") 'Subscription error'
)
result = await ingestor_manager.manage_server( result = await ingestor_manager.manage_server('slot1', 'server1', server_config, server_config)
'slot1', 'server1', server_config, server_config)
assert result == 2 assert result == 2
ingestor_manager.opc_managers["server1"].create_subscription.assert_called_once_with( ingestor_manager.opc_managers['server1'].create_subscription.assert_called_once_with('slot1')
'slot1') ingestor_manager.opc_managers['server1'].subscribe.assert_not_called()
ingestor_manager.opc_managers["server1"].subscribe.assert_not_called()
@mark.asyncio @mark.asyncio
async def test_manage_server(ingestor_manager): async def test_manage_server(ingestor_manager):
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': AsyncMock(), 'server2': AsyncMock()}
"server1": AsyncMock(), ingestor_manager.subscriptions = {'server1': AsyncMock()}
"server2": AsyncMock() server_config = {'tags': 'config1'}
}
ingestor_manager.subscriptions = {
"server1": AsyncMock()
}
server_config = {
'tags': 'config1'
}
result = await ingestor_manager.manage_server( result = await ingestor_manager.manage_server('slot1', 'server1', server_config, server_config)
'slot1', 'server1', server_config, server_config)
assert result == 0 assert result == 0
ingestor_manager.opc_managers["server1"].create_subscription.assert_called_once_with( ingestor_manager.opc_managers['server1'].create_subscription.assert_called_once_with('slot1')
'slot1') ingestor_manager.opc_managers['server1'].subscribe.assert_called_once_with(
ingestor_manager.opc_managers["server1"].subscribe.assert_called_once_with( 'slot1', 'config1', ingestor_manager.poll_interval
'slot1', 'config1', ingestor_manager.poll_interval) )
@patch('ingestor.managers.ingestor_manager.traceback') @patch('ingestor.managers.ingestor_manager.traceback')
@mark.asyncio @mark.asyncio
async def test_manage_server_subscribe_failure(traceback_mock, ingestor_manager): async def test_manage_server_subscribe_failure(traceback_mock, ingestor_manager):
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': AsyncMock(), 'server2': AsyncMock()}
"server1": AsyncMock(), ingestor_manager.subscriptions = {'server1': AsyncMock()}
"server2": AsyncMock() server_config = {'tags': 'config1'}
}
ingestor_manager.subscriptions = {
"server1": AsyncMock()
}
server_config = {
'tags': 'config1'
}
ingestor_manager.opc_managers["server1"].subscribe.side_effect = Exception( ingestor_manager.opc_managers['server1'].subscribe.side_effect = Exception('Subscription error')
"Subscription error")
result = await ingestor_manager.manage_server( result = await ingestor_manager.manage_server('slot1', 'server1', server_config, server_config)
'slot1', 'server1', server_config, server_config)
assert result == 2 assert result == 2
ingestor_manager.opc_managers["server1"].create_subscription.assert_called_once_with( ingestor_manager.opc_managers['server1'].create_subscription.assert_called_once_with('slot1')
'slot1') ingestor_manager.opc_managers['server1'].subscribe.assert_called_once_with(
ingestor_manager.opc_managers["server1"].subscribe.assert_called_once_with( 'slot1', 'config1', ingestor_manager.poll_interval
'slot1', 'config1', ingestor_manager.poll_interval) )
ingestor_manager.opc_managers["server1"].unsubscribe.assert_called_once_with( ingestor_manager.opc_managers['server1'].unsubscribe.assert_called_once_with('slot1')
'slot1')
traceback_mock.format_exc.assert_called_once() traceback_mock.format_exc.assert_called_once()
ingestor_manager.send_notification.assert_called_once_with( ingestor_manager.send_notification.assert_called_once_with(
metadata=metadata["metadata"], metadata=metadata['metadata'],
notification_id='OPC_SUBSCRIPTION_ERROR_slot1:server1', notification_id='OPC_SUBSCRIPTION_ERROR_slot1:server1',
message='Failed to subscribe to tags from slot1:server1\n{\'tags\': \'config1\'}: Subscription error', message="Failed to subscribe to tags from slot1:server1\n{'tags': 'config1'}: Subscription error",
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
attachment_content=traceback_mock.format_exc.return_value attachment_content=traceback_mock.format_exc.return_value,
) )
ingestor_manager.logger.warning.assert_any_call( ingestor_manager.logger.warning.assert_any_call(
"Removing subscription from server server1 for slot slot1" 'Removing subscription from server server1 for slot slot1'
) )
@mark.asyncio @mark.asyncio
async def test_subscribe_to_tags(ingestor_manager): async def test_subscribe_to_tags(ingestor_manager):
ingestor_manager.manage_server = AsyncMock( ingestor_manager.manage_server = AsyncMock(side_effect=[0, 1, 2])
side_effect=[0, 1, 2]) ingestor_manager.managed_tags = {'slot1': MagicMock(), 'slot2': MagicMock()}
ingestor_manager.managed_tags = {
"slot1": MagicMock(),
"slot2": MagicMock()
}
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': AsyncMock(), 'server2': AsyncMock()}
"server1": AsyncMock(), ingestor_manager.subscriptions = {'server1': AsyncMock()}
"server2": AsyncMock()
}
ingestor_manager.subscriptions = {
"server1": AsyncMock()
}
tags = { tags = {
'slot1': { 'slot1': {
"server1": {"tags": "config1"}, 'server1': {'tags': 'config1'},
"server2": {"tags": "config2"}, 'server2': {'tags': 'config2'},
'server3': {"tags": "config3"}, 'server3': {'tags': 'config3'},
} }
} }
await ingestor_manager.subscribe_to_tags(tags) await ingestor_manager.subscribe_to_tags(tags)
ingestor_manager.manage_server.assert_any_call( ingestor_manager.manage_server.assert_any_call('slot1', 'server1', {'tags': 'config1'}, tags)
'slot1', 'server1', {"tags": "config1"}, tags) ingestor_manager.manage_server.assert_any_call('slot1', 'server2', {'tags': 'config2'}, tags)
ingestor_manager.manage_server.assert_any_call( ingestor_manager.manage_server.assert_any_call('slot1', 'server3', {'tags': 'config3'}, tags)
'slot1', 'server2', {"tags": "config2"}, tags)
ingestor_manager.manage_server.assert_any_call(
'slot1', 'server3', {"tags": "config3"}, tags)
assert ingestor_manager.manage_server.call_count == 3 assert ingestor_manager.manage_server.call_count == 3
ingestor_manager.managed_tags['slot1'].pop.assert_called_once_with( ingestor_manager.managed_tags['slot1'].pop.assert_called_once_with('server3', None)
'server3', None)
@patch('ingestor.managers.ingestor_manager.metrics') @patch('ingestor.managers.ingestor_manager.metrics')
@@ -574,17 +475,14 @@ def test_check_opc_servers_integrity_all_healthy(metrics, ingestor_manager):
opc_manager1 = MagicMock() opc_manager1 = MagicMock()
opc_manager1.check_cycles.return_value = None opc_manager1.check_cycles.return_value = None
opc_manager1.check_opc_listenning.return_value = False opc_manager1.check_opc_listenning.return_value = False
opc_manager1.config = {"config": "config1"} opc_manager1.config = {'config': 'config1'}
opc_manager2 = MagicMock() opc_manager2 = MagicMock()
opc_manager2.check_cycles.return_value = None opc_manager2.check_cycles.return_value = None
opc_manager2.check_opc_listenning.return_value = False opc_manager2.check_opc_listenning.return_value = False
opc_manager2.config = {"config": "config2"} opc_manager2.config = {'config': 'config2'}
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': opc_manager1, 'server2': opc_manager2}
"server1": opc_manager1,
"server2": opc_manager2
}
# Mock the initialize_opc_from_config method # Mock the initialize_opc_from_config method
ingestor_manager.initialize_opc_from_config = MagicMock() ingestor_manager.initialize_opc_from_config = MagicMock()
@@ -601,10 +499,10 @@ def test_check_opc_servers_integrity_all_healthy(metrics, ingestor_manager):
# Verify that no reinitialization was needed # Verify that no reinitialization was needed
ingestor_manager.initialize_opc_from_config.assert_not_called() ingestor_manager.initialize_opc_from_config.assert_not_called()
metrics.OPC_MANAGERS_ACTIVE.labels.assert_called_once_with( metrics.OPC_MANAGERS_ACTIVE.labels.assert_called_once_with(pod_id=ingestor_manager.pod_id)
pod_id=ingestor_manager.pod_id)
metrics.OPC_MANAGERS_ACTIVE.labels.return_value.set.assert_called_once_with( metrics.OPC_MANAGERS_ACTIVE.labels.return_value.set.assert_called_once_with(
len(ingestor_manager.opc_managers)) len(ingestor_manager.opc_managers)
)
def test_check_opc_servers_integrity_server_lost(ingestor_manager): def test_check_opc_servers_integrity_server_lost(ingestor_manager):
@@ -612,16 +510,13 @@ def test_check_opc_servers_integrity_server_lost(ingestor_manager):
opc_manager = MagicMock() opc_manager = MagicMock()
opc_manager.check_cycles.return_value = None opc_manager.check_cycles.return_value = None
opc_manager.check_opc_listenning.return_value = True # Server is lost opc_manager.check_opc_listenning.return_value = True # Server is lost
opc_manager.config = {"config": "config1"} opc_manager.config = {'config': 'config1'}
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': opc_manager}
"server1": opc_manager
}
# Mock the initialize_opc_from_config method to return a new manager # Mock the initialize_opc_from_config method to return a new manager
new_manager = MagicMock() new_manager = MagicMock()
ingestor_manager.initialize_opc_from_config = MagicMock( ingestor_manager.initialize_opc_from_config = MagicMock(return_value=new_manager)
return_value=new_manager)
# Call the method # Call the method
ingestor_manager.check_opc_servers_integrity() ingestor_manager.check_opc_servers_integrity()
@@ -632,26 +527,18 @@ def test_check_opc_servers_integrity_server_lost_with_tags(ingestor_manager):
opc_manager = MagicMock() opc_manager = MagicMock()
opc_manager.check_cycles.return_value = None opc_manager.check_cycles.return_value = None
opc_manager.check_opc_listenning.return_value = True # Server is lost opc_manager.check_opc_listenning.return_value = True # Server is lost
opc_manager.config = {"config": "config1"} opc_manager.config = {'config': 'config1'}
ingestor_manager.opc_managers = { ingestor_manager.opc_managers = {'server1': opc_manager}
"server1": opc_manager
}
# Setup managed tags # Setup managed tags
ingestor_manager.managed_tags = { ingestor_manager.managed_tags = {
"slot1": { 'slot1': {'server1': {'config': 'config1', 'tags': {'tag1': 'value1'}}}
"server1": {
"config": "config1",
"tags": {"tag1": "value1"}
}
}
} }
# Mock the initialize_opc_from_config method to return a new manager # Mock the initialize_opc_from_config method to return a new manager
new_manager = MagicMock() new_manager = MagicMock()
ingestor_manager.initialize_opc_from_config = MagicMock( ingestor_manager.initialize_opc_from_config = MagicMock(return_value=new_manager)
return_value=new_manager)
# Call the method # Call the method
ingestor_manager.check_opc_servers_integrity() ingestor_manager.check_opc_servers_integrity()

View File

@@ -1,66 +1,68 @@
import json import json
from datetime import datetime from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, call, patch from unittest.mock import AsyncMock, MagicMock, call, patch
from pytest import fixture, mark
from asyncua.crypto.security_policies import SecurityPolicyBasic256
import pytest import pytest
from ingestor.managers.opc_manager import OpcManager from asyncua.crypto.security_policies import SecurityPolicyBasic256
from pytest import fixture, mark
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from ingestor.managers.opc_manager import OpcManager
tags = { tags = {
"ns=3;i=1001": { 'ns=3;i=1001': {
"aggregation_function": "LTS", 'aggregation_function': 'LTS',
"frequency": 1000, 'frequency': 1000,
"max_value": 100, 'max_value': 100,
"min_value": 0, 'min_value': 0,
"tag_name": "Counter", 'tag_name': 'Counter',
}, },
"ns=3;i=1003": { 'ns=3;i=1003': {
"aggregation_function": "AVG", 'aggregation_function': 'AVG',
"frequency": 1000, 'frequency': 1000,
"max_value": 100, 'max_value': 100,
"min_value": 0, 'min_value': 0,
"tag_name": "Random", 'tag_name': 'Random',
}, },
"ns=3;i=1004": { 'ns=3;i=1004': {
"aggregation_function": "MDN", 'aggregation_function': 'MDN',
"frequency": 1000, 'frequency': 1000,
"max_value": 100, 'max_value': 100,
"min_value": 0, 'min_value': 0,
"tag_name": "Sawtooth", 'tag_name': 'Sawtooth',
}, },
} }
metadata = { metadata = {
"metadata": { 'metadata': {
"model_id": "test_model", 'model_id': 'test_model',
"model_name": "test_model", 'model_name': 'test_model',
"workflow_name": "test_workflow", 'workflow_name': 'test_workflow',
"schema_name": "test_schedule", 'schema_name': 'test_schedule',
}, },
} }
@fixture @fixture
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
def raw_opc_manager(mock_metrics): def raw_opc_manager(mock_metrics):
return OpcManager( return OpcManager(
name="TestConnector", name='TestConnector',
url="opc.tcp://localhost:4840", url='opc.tcp://localhost:4840',
data_manager=MagicMock(), data_manager=MagicMock(),
logger=MagicMock(), logger=MagicMock(),
server_uri="opc.tcp://localhost:4840", server_uri='opc.tcp://localhost:4840',
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
@fixture @fixture
def opc_manager(raw_opc_manager): def opc_manager(raw_opc_manager):
raw_opc_manager.client = AsyncMock() raw_opc_manager.client = AsyncMock()
raw_opc_manager.cert_path = "cert.pem" raw_opc_manager.cert_path = 'cert.pem'
raw_opc_manager.private_key_path = "private_key.pem" raw_opc_manager.private_key_path = 'private_key.pem'
raw_opc_manager.server_cert_path = "server_cert.pem" raw_opc_manager.server_cert_path = 'server_cert.pem'
raw_opc_manager.send_notification = MagicMock() raw_opc_manager.send_notification = MagicMock()
return raw_opc_manager return raw_opc_manager
@@ -68,7 +70,7 @@ def opc_manager(raw_opc_manager):
@fixture @fixture
def opc_manager_subscribed(opc_manager): def opc_manager_subscribed(opc_manager):
opc_manager.subscriptions["sub1"] = AsyncMock() opc_manager.subscriptions['sub1'] = AsyncMock()
return opc_manager return opc_manager
@@ -76,7 +78,7 @@ def opc_manager_subscribed(opc_manager):
def test___str__(opc_manager): def test___str__(opc_manager):
assert ( assert (
str(opc_manager) str(opc_manager)
== "OpcManager(name=TestConnector, url=opc.tcp://localhost:4840, server_uri=opc.tcp://localhost:4840)\nnodes={}, subscriptions={}" == 'OpcManager(name=TestConnector, url=opc.tcp://localhost:4840, server_uri=opc.tcp://localhost:4840)\nnodes={}, subscriptions={}'
) )
@@ -91,21 +93,18 @@ async def test_shutdown_success(opc_manager):
@mark.asyncio @mark.asyncio
async def test_shutdown_error(opc_manager): async def test_shutdown_error(opc_manager):
opc_manager.disconnect = AsyncMock(side_effect=Exception("Test error")) opc_manager.disconnect = AsyncMock(side_effect=Exception('Test error'))
await opc_manager.shutdown() await opc_manager.shutdown()
opc_manager.logger.error.assert_called_once_with( opc_manager.logger.error.assert_called_once_with('Error during cleanup: Test error')
"Error during cleanup: Test error")
@mark.asyncio @mark.asyncio
async def test_set_security_success(opc_manager): async def test_set_security_success(opc_manager):
await opc_manager.set_security() await opc_manager.set_security()
opc_manager.client.set_application_uri.assert_called_once_with( opc_manager.client.set_application_uri.assert_called_once_with(opc_manager.server_uri)
opc_manager.server_uri
)
opc_manager.client.set_security.assert_called_once_with( opc_manager.client.set_security.assert_called_once_with(
SecurityPolicyBasic256, SecurityPolicyBasic256,
@@ -114,8 +113,7 @@ async def test_set_security_success(opc_manager):
server_certificate=opc_manager.server_cert_path, server_certificate=opc_manager.server_cert_path,
) )
opc_manager.client.set_secure_channel_timeout.assert_called_once_with( opc_manager.client.set_secure_channel_timeout.assert_called_once_with(10000000)
10000000)
opc_manager.client.set_session_timeout.assert_called_once_with(10000000) opc_manager.client.set_session_timeout.assert_called_once_with(10000000)
@@ -127,19 +125,16 @@ async def test_set_security_no_cert(opc_manager):
try: try:
await opc_manager.set_security() await opc_manager.set_security()
except ValueError as e: except ValueError as e:
assert ( assert str(e) == 'Certificate and private key paths must be provided for secure connection.'
str(e)
== "Certificate and private key paths must be provided for secure connection."
)
else: else:
assert False, "ValueError not raised" raise AssertionError('ValueError not raised')
assert opc_manager.client.set_security.call_count == 0 assert opc_manager.client.set_security.call_count == 0
@mark.asyncio @mark.asyncio
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
@patch("ingestor.managers.opc_manager.Client") @patch('ingestor.managers.opc_manager.Client')
async def test_connect_no_security(client, mock_metrics, raw_opc_manager): async def test_connect_no_security(client, mock_metrics, raw_opc_manager):
raw_opc_manager.set_security = AsyncMock() raw_opc_manager.set_security = AsyncMock()
client.return_value = AsyncMock() client.return_value = AsyncMock()
@@ -150,26 +145,24 @@ async def test_connect_no_security(client, mock_metrics, raw_opc_manager):
raw_opc_manager.client.connect.assert_called_once() raw_opc_manager.client.connect.assert_called_once()
raw_opc_manager.set_security.assert_not_called() raw_opc_manager.set_security.assert_not_called()
mock_metrics.OPC_CONNECTIONS_TOTAL.labels.assert_called_once_with( mock_metrics.OPC_CONNECTIONS_TOTAL.labels.assert_called_once_with(
pod_id=raw_opc_manager.pod_id, pod_id=raw_opc_manager.pod_id, server_name=raw_opc_manager.name
server_name=raw_opc_manager.name
) )
mock_metrics.OPC_CONNECTIONS_TOTAL.labels.return_value.inc.assert_called_once() mock_metrics.OPC_CONNECTIONS_TOTAL.labels.return_value.inc.assert_called_once()
mock_metrics.OPC_CONNECTION_STATUS.labels.assert_called_once_with( mock_metrics.OPC_CONNECTION_STATUS.labels.assert_called_once_with(
pod_id=raw_opc_manager.pod_id, pod_id=raw_opc_manager.pod_id,
server_name=raw_opc_manager.name, server_name=raw_opc_manager.name,
server_url=raw_opc_manager.url server_url=raw_opc_manager.url,
) )
mock_metrics.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with( mock_metrics.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with(1)
1)
mock_metrics.OPC_CONNECTIONS_FAILED.labels.assert_not_called() mock_metrics.OPC_CONNECTIONS_FAILED.labels.assert_not_called()
@mark.asyncio @mark.asyncio
@patch("ingestor.managers.opc_manager.Client") @patch('ingestor.managers.opc_manager.Client')
async def test_connect_with_security(client, raw_opc_manager): async def test_connect_with_security(client, raw_opc_manager):
raw_opc_manager.cert_path = "cert.pem" raw_opc_manager.cert_path = 'cert.pem'
raw_opc_manager.private_key_path = "private_key.pem" raw_opc_manager.private_key_path = 'private_key.pem'
raw_opc_manager.server_cert_path = "server_cert.pem" raw_opc_manager.server_cert_path = 'server_cert.pem'
raw_opc_manager.set_security = AsyncMock() raw_opc_manager.set_security = AsyncMock()
client.return_value = AsyncMock() client.return_value = AsyncMock()
@@ -181,15 +174,14 @@ async def test_connect_with_security(client, raw_opc_manager):
@mark.asyncio @mark.asyncio
@patch("ingestor.managers.opc_manager.Client") @patch('ingestor.managers.opc_manager.Client')
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
async def test_connect_exception_handling_and_metrics( async def test_connect_exception_handling_and_metrics(
mock_metrics_module, mock_opc_client_class, raw_opc_manager mock_metrics_module, mock_opc_client_class, raw_opc_manager
): ):
mock_client_instance = mock_opc_client_class.return_value mock_client_instance = mock_opc_client_class.return_value
simulated_error_message = "Erro de conexão simulado" simulated_error_message = 'Erro de conexão simulado'
mock_client_instance.connect.side_effect = Exception( mock_client_instance.connect.side_effect = Exception(simulated_error_message)
simulated_error_message)
opc_manager_instance = raw_opc_manager opc_manager_instance = raw_opc_manager
opc_manager_instance.cert_path = None opc_manager_instance.cert_path = None
@@ -207,9 +199,7 @@ async def test_connect_exception_handling_and_metrics(
server_name=opc_manager_instance.name, server_name=opc_manager_instance.name,
server_url=opc_manager_instance.url, server_url=opc_manager_instance.url,
) )
mock_metrics_module.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with( mock_metrics_module.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with(0)
0
)
mock_metrics_module.OPC_CONNECTIONS_FAILED.labels.assert_called_once_with( mock_metrics_module.OPC_CONNECTIONS_FAILED.labels.assert_called_once_with(
pod_id=opc_manager_instance.pod_id, server_name=opc_manager_instance.name pod_id=opc_manager_instance.pod_id, server_name=opc_manager_instance.name
@@ -217,50 +207,48 @@ async def test_connect_exception_handling_and_metrics(
mock_metrics_module.OPC_CONNECTIONS_FAILED.labels.return_value.inc.assert_called_once() mock_metrics_module.OPC_CONNECTIONS_FAILED.labels.return_value.inc.assert_called_once()
opc_manager_instance.logger.error.assert_called_once_with( opc_manager_instance.logger.error.assert_called_once_with(
f"Failed to connect to {opc_manager_instance.name}: {simulated_error_message}" f'Failed to connect to {opc_manager_instance.name}: {simulated_error_message}'
) )
@mark.asyncio @mark.asyncio
async def test_create_subscription_no_client(raw_opc_manager): async def test_create_subscription_no_client(raw_opc_manager):
try: try:
await raw_opc_manager.create_subscription("sub1") await raw_opc_manager.create_subscription('sub1')
except ValueError as e: except ValueError as e:
assert str(e) == "Client not connected. Call connect first." assert str(e) == 'Client not connected. Call connect first.'
else: else:
assert False, "ValueError not raised" raise AssertionError('ValueError not raised')
@mark.asyncio @mark.asyncio
async def test_create_subscription_success_has_period(opc_manager): async def test_create_subscription_success_has_period(opc_manager):
await opc_manager.create_subscription("sub1", 1000) await opc_manager.create_subscription('sub1', 1000)
opc_manager.client.create_subscription.assert_called_once_with( opc_manager.client.create_subscription.assert_called_once_with(1000, opc_manager)
1000, opc_manager) assert opc_manager.subscriptions['sub1'] is not None
assert opc_manager.subscriptions["sub1"] is not None
@mark.asyncio @mark.asyncio
async def test_create_subscription_success_no_period(opc_manager): async def test_create_subscription_success_no_period(opc_manager):
await opc_manager.create_subscription("sub1", None) await opc_manager.create_subscription('sub1', None)
opc_manager.client.create_subscription.assert_called_once_with( opc_manager.client.create_subscription.assert_called_once_with(500, opc_manager)
500, opc_manager) assert opc_manager.subscriptions['sub1'] is not None
assert opc_manager.subscriptions["sub1"] is not None
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
@mark.asyncio @mark.asyncio
async def test_create_subscription_with_metrics(metrics, opc_manager): async def test_create_subscription_with_metrics(metrics, opc_manager):
await opc_manager.create_subscription("sub1", 1000) await opc_manager.create_subscription('sub1', 1000)
metrics.OPC_SUBSCRIPTIONS_CREATED.labels.assert_called_once_with( metrics.OPC_SUBSCRIPTIONS_CREATED.labels.assert_called_once_with(
pod_id=opc_manager.pod_id, server_name=opc_manager.name, slot_name="sub1" pod_id=opc_manager.pod_id, server_name=opc_manager.name, slot_name='sub1'
) )
metrics.OPC_SUBSCRIPTIONS_CREATED.labels.return_value.inc.assert_called_once() metrics.OPC_SUBSCRIPTIONS_CREATED.labels.return_value.inc.assert_called_once()
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
@mark.asyncio @mark.asyncio
async def test_create_subscription_exception_during_client_call( async def test_create_subscription_exception_during_client_call(
mock_metrics_module, raw_opc_manager mock_metrics_module, raw_opc_manager
@@ -268,71 +256,66 @@ async def test_create_subscription_exception_during_client_call(
opc_manager_instance = raw_opc_manager opc_manager_instance = raw_opc_manager
opc_manager_instance.client = AsyncMock() opc_manager_instance.client = AsyncMock()
subscription_name = "test_sub_client_error" subscription_name = 'test_sub_client_error'
simulated_period = 750 simulated_period = 750
simulated_error_message = "Falha ao criar subscrição no cliente OPC" simulated_error_message = 'Falha ao criar subscrição no cliente OPC'
opc_manager_instance.client.create_subscription.side_effect = Exception( opc_manager_instance.client.create_subscription.side_effect = Exception(simulated_error_message)
simulated_error_message
)
with pytest.raises(Exception, match=simulated_error_message): with pytest.raises(Exception, match=simulated_error_message):
await opc_manager_instance.create_subscription( await opc_manager_instance.create_subscription(subscription_name, period=simulated_period)
subscription_name, period=simulated_period
)
opc_manager_instance.client.create_subscription.assert_called_once_with( opc_manager_instance.client.create_subscription.assert_called_once_with(
simulated_period, opc_manager_instance simulated_period, opc_manager_instance
) )
opc_manager_instance.logger.error.assert_called_once_with( opc_manager_instance.logger.error.assert_called_once_with(
f"Failed to create subscription {subscription_name} on {opc_manager_instance.name}: {simulated_error_message}" f'Failed to create subscription {subscription_name} on {opc_manager_instance.name}: {simulated_error_message}'
) )
mock_metrics_module.OPC_SUBSCRIPTIONS_CREATED.labels.assert_not_called() mock_metrics_module.OPC_SUBSCRIPTIONS_CREATED.labels.assert_not_called()
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
@mark.asyncio @mark.asyncio
async def test_subscribe_no_subscription(metrics, opc_manager): async def test_subscribe_no_subscription(metrics, opc_manager):
try: try:
await opc_manager.subscribe("sub1", tags, 1000) await opc_manager.subscribe('sub1', tags, 1000)
except ValueError as e: except ValueError as e:
assert str( assert str(e) == 'Subscription not created. Call create_subscription first.'
e) == "Subscription not created. Call create_subscription first."
else: else:
assert False, "ValueError not raised" raise AssertionError('ValueError not raised')
metrics.OPC_TAGS_SUBSCRIBED.labels.assert_not_called() metrics.OPC_TAGS_SUBSCRIBED.labels.assert_not_called()
@mark.asyncio @mark.asyncio
async def test_subscribe_success(opc_manager_subscribed): async def test_subscribe_success(opc_manager_subscribed):
opc_manager_subscribed.client.get_node = MagicMock() opc_manager_subscribed.client.get_node = MagicMock()
opc_manager_subscribed.nodes = {"ns=3;i=1001": "data"} opc_manager_subscribed.nodes = {'ns=3;i=1001': 'data'}
await opc_manager_subscribed.subscribe("sub1", tags, 1000) await opc_manager_subscribed.subscribe('sub1', tags, 1000)
assert opc_manager_subscribed.nodes == tags assert opc_manager_subscribed.nodes == tags
opc_manager_subscribed.subscriptions["sub1"].subscribe_data_change.assert_called_once_with( opc_manager_subscribed.subscriptions['sub1'].subscribe_data_change.assert_called_once_with(
[opc_manager_subscribed.client.get_node(n) for n in tags] [opc_manager_subscribed.client.get_node(n) for n in tags]
) )
@mark.asyncio @mark.asyncio
async def test_unsubscribe_no_subscription(opc_manager): async def test_unsubscribe_no_subscription(opc_manager):
await opc_manager.unsubscribe("sub1") await opc_manager.unsubscribe('sub1')
opc_manager.logger.warning.assert_called_once_with( opc_manager.logger.warning.assert_called_once_with(
"Subscription 'sub1' not found. Cannot unsubscribe." "Subscription 'sub1' not found. Cannot unsubscribe."
) )
assert opc_manager.subscriptions.get("sub1") is None assert opc_manager.subscriptions.get('sub1') is None
@mark.asyncio @mark.asyncio
async def test_unsubscribe_success(opc_manager_subscribed): async def test_unsubscribe_success(opc_manager_subscribed):
await opc_manager_subscribed.unsubscribe("sub1") await opc_manager_subscribed.unsubscribe('sub1')
opc_manager_subscribed.subscriptions.get("sub1") is None assert opc_manager_subscribed.subscriptions.get('sub1') is None
@mark.asyncio @mark.asyncio
@@ -341,7 +324,7 @@ async def test_disconnect_success(opc_manager_subscribed):
await opc_manager_subscribed.disconnect() await opc_manager_subscribed.disconnect()
opc_manager_subscribed.subscriptions["sub1"].delete.assert_called_once() opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once()
assert opc_manager_subscribed.client is None assert opc_manager_subscribed.client is None
@@ -352,42 +335,38 @@ async def test_disconnect_no_client(opc_manager_subscribed):
opc_manager_subscribed.logger.warning.assert_has_calls( opc_manager_subscribed.logger.warning.assert_has_calls(
[ [
call("Client already disconnected."), call('Client already disconnected.'),
] ]
) )
@mark.asyncio @mark.asyncio
async def test_disconnect_error_unsubscribe(opc_manager_subscribed): async def test_disconnect_error_unsubscribe(opc_manager_subscribed):
opc_manager_subscribed.client = MagicMock( opc_manager_subscribed.client = MagicMock(disconnect=AsyncMock())
disconnect=AsyncMock() opc_manager_subscribed.subscriptions['sub1'] = MagicMock(
) delete=AsyncMock(side_effect=Exception('Test error'))
opc_manager_subscribed.subscriptions["sub1"] = MagicMock(
delete=AsyncMock(side_effect=Exception("Test error"))
) )
await opc_manager_subscribed.disconnect() await opc_manager_subscribed.disconnect()
opc_manager_subscribed.subscriptions["sub1"].delete.assert_called_once() opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once()
opc_manager_subscribed.client = None opc_manager_subscribed.client = None
opc_manager_subscribed.logger.error.assert_called_once_with( opc_manager_subscribed.logger.error.assert_called_once_with(
"Failed to clean up subscription: Test error" 'Failed to clean up subscription: Test error'
) )
@mark.asyncio @mark.asyncio
async def test_disconnect_error(opc_manager_subscribed): async def test_disconnect_error(opc_manager_subscribed):
opc_manager_subscribed.client = MagicMock() opc_manager_subscribed.client = MagicMock()
opc_manager_subscribed.client.disconnect = MagicMock( opc_manager_subscribed.client.disconnect = MagicMock(side_effect=Exception('Test error'))
side_effect=Exception("Test error")
)
await opc_manager_subscribed.disconnect() await opc_manager_subscribed.disconnect()
opc_manager_subscribed.subscriptions["sub1"].delete.assert_called_once() opc_manager_subscribed.subscriptions['sub1'].delete.assert_called_once()
opc_manager_subscribed.client = None opc_manager_subscribed.client = None
opc_manager_subscribed.logger.error.assert_called_once_with( opc_manager_subscribed.logger.error.assert_called_once_with(
"Failed to disconnect from OPC UA server: Test error" 'Failed to disconnect from OPC UA server: Test error'
) )
@@ -400,24 +379,21 @@ async def test_disconnect_metrics_on_successful_path(mock_metrics_module, raw_op
raw_opc_manager.client = MagicMock() raw_opc_manager.client = MagicMock()
mock_sub1 = MagicMock() mock_sub1 = MagicMock()
mock_sub2 = MagicMock() mock_sub2 = MagicMock()
raw_opc_manager.subscriptions = {"sub1": mock_sub1, "sub2": mock_sub2} raw_opc_manager.subscriptions = {'sub1': mock_sub1, 'sub2': mock_sub2}
await raw_opc_manager.disconnect() await raw_opc_manager.disconnect()
mock_metrics_module.OPC_CONNECTION_STATUS.labels.assert_called_once_with( mock_metrics_module.OPC_CONNECTION_STATUS.labels.assert_called_once_with(
pod_id=raw_opc_manager.pod_id, pod_id=raw_opc_manager.pod_id,
server_name=raw_opc_manager.name, server_name=raw_opc_manager.name,
server_url=raw_opc_manager.url server_url=raw_opc_manager.url,
) )
mock_metrics_module.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with( mock_metrics_module.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with(0)
0)
mock_metrics_module.OPC_TAGS_SUBSCRIBED.labels.assert_called_once_with( mock_metrics_module.OPC_TAGS_SUBSCRIBED.labels.assert_called_once_with(
pod_id=raw_opc_manager.pod_id, pod_id=raw_opc_manager.pod_id, server_name=raw_opc_manager.name
server_name=raw_opc_manager.name
) )
mock_metrics_module.OPC_TAGS_SUBSCRIBED.labels.return_value.set.assert_called_once_with( mock_metrics_module.OPC_TAGS_SUBSCRIBED.labels.return_value.set.assert_called_once_with(0)
0)
@patch('ingestor.managers.opc_manager.metrics') @patch('ingestor.managers.opc_manager.metrics')
@@ -427,76 +403,70 @@ async def test_datachange_notification(metrics, opc_manager_subscribed):
monitored_item=MagicMock( monitored_item=MagicMock(
Value=MagicMock( Value=MagicMock(
Value=MagicMock(Value=42), Value=MagicMock(Value=42),
SourceTimestamp=datetime.strptime( SourceTimestamp=datetime.strptime('2021-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S'),
"2021-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S"
),
) )
) )
) )
opc_manager_subscribed.nodes = { opc_manager_subscribed.nodes = {
"ns=3;i=1001": { 'ns=3;i=1001': {
"tag_name": "Counter", 'tag_name': 'Counter',
"cycle_rule": {"cycle_increment": 1.0, "cycle_count": 2}, 'cycle_rule': {'cycle_increment': 1.0, 'cycle_count': 2},
"topics": ["topic1", "topic2"], 'topics': ['topic1', 'topic2'],
} }
} }
metrics.OPC_CYCLES_WITHOUT_DATA.reset_mock() metrics.OPC_CYCLES_WITHOUT_DATA.reset_mock()
await opc_manager_subscribed.datachange_notification("ns=3;i=1001", None, data) await opc_manager_subscribed.datachange_notification('ns=3;i=1001', None, data)
opc_manager_subscribed.data_manager.publish.assert_any_call( opc_manager_subscribed.data_manager.publish.assert_any_call(
"topic1", 'topic1',
{ {
"tag": "ns=3;i=1001", 'tag': 'ns=3;i=1001',
"name": "Counter", 'name': 'Counter',
"timestamp": "2021-01-01 00:00:00-0300", 'timestamp': '2021-01-01 00:00:00-0300',
"value": 42, 'value': 42,
}, },
) )
opc_manager_subscribed.data_manager.publish.assert_any_call( opc_manager_subscribed.data_manager.publish.assert_any_call(
"topic2", 'topic2',
{ {
"tag": "ns=3;i=1001", 'tag': 'ns=3;i=1001',
"name": "Counter", 'name': 'Counter',
"timestamp": "2021-01-01 00:00:00-0300", 'timestamp': '2021-01-01 00:00:00-0300',
"value": 42, 'value': 42,
}, },
) )
assert opc_manager_subscribed.nodes["ns=3;i=1001"]["cycle_rule"]["cycle_count"] == 0 assert opc_manager_subscribed.nodes['ns=3;i=1001']['cycle_rule']['cycle_count'] == 0
metrics.OPC_CYCLES_WITHOUT_DATA.labels.assert_called_once_with( metrics.OPC_CYCLES_WITHOUT_DATA.labels.assert_called_once_with(
pod_id=opc_manager_subscribed.pod_id, pod_id=opc_manager_subscribed.pod_id, server_name=opc_manager_subscribed.name
server_name=opc_manager_subscribed.name
) )
metrics.OPC_CYCLES_WITHOUT_DATA.labels.return_value.set.assert_called_once_with( metrics.OPC_CYCLES_WITHOUT_DATA.labels.return_value.set.assert_called_once_with(0)
0)
def test_check_cycles_no_notification(opc_manager): def test_check_cycles_no_notification(opc_manager):
# Setup: node with cycle_count just below threshold # Setup: node with cycle_count just below threshold
opc_manager.nodes = { opc_manager.nodes = {
"ns=3;i=1001": { 'ns=3;i=1001': {
"tag_name": "Counter", 'tag_name': 'Counter',
"cycle_rule": {"cycle_increment": 1.0, "cycle_count": 3.0}, 'cycle_rule': {'cycle_increment': 1.0, 'cycle_count': 3.0},
} }
} }
opc_manager.check_cycles() opc_manager.check_cycles()
# After one increment, cycle_count = 4.0, still below threshold # After one increment, cycle_count = 4.0, still below threshold
assert opc_manager.nodes["ns=3;i=1001"]["cycle_rule"][ assert opc_manager.nodes['ns=3;i=1001']['cycle_rule']['cycle_count'] == pytest.approx(4.0)
"cycle_count"
] == pytest.approx(4.0)
opc_manager.send_notification.assert_not_called() opc_manager.send_notification.assert_not_called()
def test_check_cycles_triggers_notification(opc_manager): def test_check_cycles_triggers_notification(opc_manager):
# Setup: node with cycle_count just below threshold, increment will cross threshold # Setup: node with cycle_count just below threshold, increment will cross threshold
opc_manager.nodes = { opc_manager.nodes = {
"ns=3;i=1001": { 'ns=3;i=1001': {
"tag_name": "Counter", 'tag_name': 'Counter',
"cycle_rule": {"cycle_increment": 2.5, "cycle_count": 3.0}, 'cycle_rule': {'cycle_increment': 2.5, 'cycle_count': 3.0},
} }
} }
opc_manager.notification_handler.build_and_send_notification = MagicMock() opc_manager.notification_handler.build_and_send_notification = MagicMock()
@@ -504,15 +474,13 @@ def test_check_cycles_triggers_notification(opc_manager):
opc_manager.check_cycles() opc_manager.check_cycles()
# After increment, cycle_count = 5.5, should trigger notification # After increment, cycle_count = 5.5, should trigger notification
assert opc_manager.nodes["ns=3;i=1001"]["cycle_rule"][ assert opc_manager.nodes['ns=3;i=1001']['cycle_rule']['cycle_count'] == pytest.approx(5.5)
"cycle_count"
] == pytest.approx(5.5)
opc_manager.send_notification.assert_called_once_with( opc_manager.send_notification.assert_called_once_with(
notification_id="TAG_ns=3;i=1001:Counter_LISTENNING_STOPPED", notification_id='TAG_ns=3;i=1001:Counter_LISTENNING_STOPPED',
message="5.5 cycles without receive from ns=3;i=1001:Counter", message='5.5 cycles without receive from ns=3;i=1001:Counter',
block="opc_manager", block='opc_manager',
level=NotificationLevel.WARNING, level=NotificationLevel.WARNING,
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
@@ -530,11 +498,11 @@ def test_check_opc_listenning_no_notification(metrics, opc_manager):
assert result is False assert result is False
metrics.OPC_CYCLES_WITHOUT_DATA.labels.assert_called_once_with( metrics.OPC_CYCLES_WITHOUT_DATA.labels.assert_called_once_with(
pod_id=opc_manager.pod_id, pod_id=opc_manager.pod_id, server_name=opc_manager.name
server_name=opc_manager.name
) )
metrics.OPC_CYCLES_WITHOUT_DATA.labels.return_value.set.assert_called_once_with( metrics.OPC_CYCLES_WITHOUT_DATA.labels.return_value.set.assert_called_once_with(
opc_manager.non_receive_count) opc_manager.non_receive_count
)
metrics.OPC_RECONNECTIONS_TOTAL.labels.assert_not_called() metrics.OPC_RECONNECTIONS_TOTAL.labels.assert_not_called()
@@ -545,11 +513,11 @@ def test_check_opc_listenning_warning_notification(opc_manager):
assert opc_manager.non_receive_count == 5 assert opc_manager.non_receive_count == 5
opc_manager.send_notification.assert_called_once_with( opc_manager.send_notification.assert_called_once_with(
notification_id=f"OPC_LISTENNING_STOPPED__{opc_manager.name}", notification_id=f'OPC_LISTENNING_STOPPED__{opc_manager.name}',
message=f"5 cycles without receive from OPC {opc_manager.name}. Tags: {json.dumps(opc_manager.nodes)}", message=f'5 cycles without receive from OPC {opc_manager.name}. Tags: {json.dumps(opc_manager.nodes)}',
block="opc_manager", block='opc_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
assert result is False assert result is False
@@ -569,47 +537,45 @@ def test_check_opc_listenning_error_notification_and_retry(metrics, opc_manager)
calls = opc_manager.send_notification.call_args_list calls = opc_manager.send_notification.call_args_list
# First call: 5 cycles warning # First call: 5 cycles warning
assert calls[0].kwargs == { assert calls[0].kwargs == {
"notification_id": f"OPC_LISTENNING_STOPPED__{opc_manager.name}", 'notification_id': f'OPC_LISTENNING_STOPPED__{opc_manager.name}',
"message": f"15 cycles without receive from OPC {opc_manager.name}. Tags: {json.dumps(opc_manager.nodes)}", 'message': f'15 cycles without receive from OPC {opc_manager.name}. Tags: {json.dumps(opc_manager.nodes)}',
"block": "opc_manager", 'block': 'opc_manager',
"level": NotificationLevel.ERROR, 'level': NotificationLevel.ERROR,
"metadata": metadata["metadata"], 'metadata': metadata['metadata'],
} }
# Second call: 15 cycles retry # Second call: 15 cycles retry
assert calls[1].kwargs == { assert calls[1].kwargs == {
"notification_id": f"OPC_CONNECTION_RETRY__{opc_manager.name}", 'notification_id': f'OPC_CONNECTION_RETRY__{opc_manager.name}',
"message": f"Retrying to connect to server {opc_manager.name}", 'message': f'Retrying to connect to server {opc_manager.name}',
"block": "opc_manager", 'block': 'opc_manager',
"level": NotificationLevel.ERROR, 'level': NotificationLevel.ERROR,
"metadata": metadata["metadata"], 'metadata': metadata['metadata'],
} }
assert result is True assert result is True
metrics.OPC_CYCLES_WITHOUT_DATA.labels.assert_called_once_with( metrics.OPC_CYCLES_WITHOUT_DATA.labels.assert_called_once_with(
pod_id=opc_manager.pod_id, pod_id=opc_manager.pod_id, server_name=opc_manager.name
server_name=opc_manager.name
) )
metrics.OPC_CYCLES_WITHOUT_DATA.labels.return_value.set.assert_called_once_with( metrics.OPC_CYCLES_WITHOUT_DATA.labels.return_value.set.assert_called_once_with(
opc_manager.non_receive_count) opc_manager.non_receive_count
)
metrics.OPC_RECONNECTIONS_TOTAL.labels.assert_called_once_with( metrics.OPC_RECONNECTIONS_TOTAL.labels.assert_called_once_with(
pod_id=opc_manager.pod_id, pod_id=opc_manager.pod_id, server_name=opc_manager.name
server_name=opc_manager.name
) )
metrics.OPC_RECONNECTIONS_TOTAL.labels.return_value.inc.assert_called_once() metrics.OPC_RECONNECTIONS_TOTAL.labels.return_value.inc.assert_called_once()
@patch("ingestor.managers.opc_manager.metrics") @patch('ingestor.managers.opc_manager.metrics')
def test_init_metrics_calls_correct_metric_methods(metrics): def test_init_metrics_calls_correct_metric_methods(metrics):
opc_manager = OpcManager( opc_manager = OpcManager(
name="TestInitConnector", name='TestInitConnector',
url="opc.tcp://init.test:4840", url='opc.tcp://init.test:4840',
data_manager=MagicMock(), data_manager=MagicMock(),
logger=MagicMock(), logger=MagicMock(),
server_uri="opc.tcp://init.test:4840/uri", server_uri='opc.tcp://init.test:4840/uri',
notification_handler=MagicMock(), notification_handler=MagicMock(),
metadata=metadata["metadata"], metadata=metadata['metadata'],
) )
metrics.OPC_CONNECTION_STATUS.labels.assert_called_with( metrics.OPC_CONNECTION_STATUS.labels.assert_called_with(
@@ -618,13 +584,10 @@ def test_init_metrics_calls_correct_metric_methods(metrics):
server_url=opc_manager.url, server_url=opc_manager.url,
) )
metrics.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with( metrics.OPC_CONNECTION_STATUS.labels.return_value.set.assert_called_once_with(0)
0)
metrics.OPC_TAGS_SUBSCRIBED.labels.assert_called_once_with( metrics.OPC_TAGS_SUBSCRIBED.labels.assert_called_once_with(
pod_id=opc_manager.pod_id, pod_id=opc_manager.pod_id, server_name=opc_manager.name
server_name=opc_manager.name
) )
metrics.OPC_TAGS_SUBSCRIBED.labels.return_value.set.assert_called_once_with( metrics.OPC_TAGS_SUBSCRIBED.labels.return_value.set.assert_called_once_with(0)
0)

View File

@@ -1,28 +1,29 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from pytest import fixture, raises from pytest import fixture, raises
from sientia_do.notifications.models import NotificationLevel from sientia_do.notifications.models import NotificationLevel
from ingestor.managers.resource_manager import ResourceManager from ingestor.managers.resource_manager import ResourceManager
metadata = { metadata = {
"metadata": { 'metadata': {
"model_id": "test_model", 'model_id': 'test_model',
"model_name": "test_model", 'model_name': 'test_model',
"workflow_name": "test_workflow", 'workflow_name': 'test_workflow',
"schema_name": "test_schedule", 'schema_name': 'test_schedule',
}, },
} }
@fixture @fixture
@patch("ingestor.managers.resource_manager.Redis") @patch('ingestor.managers.resource_manager.Redis')
def resource_manager(redis): def resource_manager(redis):
resource_manager = ResourceManager( resource_manager = ResourceManager(
host="localhost", host='localhost',
port=6379, port=6379,
lease_ttl=10, lease_ttl=10,
heartbeat_ttl=10, heartbeat_ttl=10,
metadata=metadata["metadata"], metadata=metadata['metadata'],
logger=MagicMock(), logger=MagicMock(),
notification_handler=MagicMock(), notification_handler=MagicMock(),
) )
@@ -34,152 +35,147 @@ def resource_manager(redis):
def test_get_success(resource_manager): def test_get_success(resource_manager):
resource_manager.redis.get.return_value = '{"key": "value"}' resource_manager.redis.get.return_value = '{"key": "value"}'
result = resource_manager.get("key") result = resource_manager.get('key')
assert result == {"key": "value"} assert result == {'key': 'value'}
resource_manager.redis.get.assert_called_once_with("key") resource_manager.redis.get.assert_called_once_with('key')
def test_get_failure(resource_manager): def test_get_failure(resource_manager):
resource_manager.redis.get.return_value = None resource_manager.redis.get.return_value = None
result = resource_manager.get("key") result = resource_manager.get('key')
assert result is None assert result is None
resource_manager.redis.get.assert_called_once_with("key") resource_manager.redis.get.assert_called_once_with('key')
def test_get_tag_slot(resource_manager): def test_get_tag_slot(resource_manager):
resource_manager.get = MagicMock(return_value={"tag": "slot"}) resource_manager.get = MagicMock(return_value={'tag': 'slot'})
result = resource_manager.get_tag_slot("id") result = resource_manager.get_tag_slot('id')
assert result == {"tag": "slot"} assert result == {'tag': 'slot'}
resource_manager.get.assert_called_once_with("slot:opc_tags:id") resource_manager.get.assert_called_once_with('slot:opc_tags:id')
def test_ingestor_heartbeat(resource_manager): def test_ingestor_heartbeat(resource_manager):
resource_manager.redis.set.return_value = True resource_manager.redis.set.return_value = True
resource_manager.ingestor_heartbeat() resource_manager.ingestor_heartbeat()
resource_manager.redis.set.assert_called_once_with( resource_manager.redis.set.assert_called_once_with('heartbeat:ingestor:localhost', 1, ex=10)
"heartbeat:ingestor:localhost", 1, ex=10
)
def test_lease_tag(resource_manager): def test_lease_tag(resource_manager):
resource_manager.redis.set.return_value = True resource_manager.redis.set.return_value = True
output = resource_manager.lease_tag("tag_id") output = resource_manager.lease_tag('tag_id')
assert output is True assert output is True
resource_manager.redis.set.assert_called_once_with( resource_manager.redis.set.assert_called_once_with(
"lease:opc_tags:tag_id", "localhost", nx=True, ex=10 'lease:opc_tags:tag_id', 'localhost', nx=True, ex=10
) )
def test_renew_tag_lease_success(resource_manager): def test_renew_tag_lease_success(resource_manager):
resource_manager.redis.get.return_value = "localhost" resource_manager.redis.get.return_value = 'localhost'
resource_manager.redis.expire.return_value = True resource_manager.redis.expire.return_value = True
result = resource_manager.renew_tag_lease("tag_id") result = resource_manager.renew_tag_lease('tag_id')
assert result is True assert result is True
resource_manager.redis.get.assert_called_once_with("lease:opc_tags:tag_id") resource_manager.redis.get.assert_called_once_with('lease:opc_tags:tag_id')
resource_manager.redis.expire.assert_called_once_with( resource_manager.redis.expire.assert_called_once_with('lease:opc_tags:tag_id', 10)
"lease:opc_tags:tag_id", 10)
def test_renew_tag_lease_failure(resource_manager): def test_renew_tag_lease_failure(resource_manager):
resource_manager.redis.get.return_value = "other_pod_id" resource_manager.redis.get.return_value = 'other_pod_id'
resource_manager.redis.expire.return_value = False resource_manager.redis.expire.return_value = False
result = resource_manager.renew_tag_lease("tag_id") result = resource_manager.renew_tag_lease('tag_id')
assert result is False assert result is False
resource_manager.redis.get.assert_called_once_with("lease:opc_tags:tag_id") resource_manager.redis.get.assert_called_once_with('lease:opc_tags:tag_id')
resource_manager.redis.expire.assert_not_called() resource_manager.redis.expire.assert_not_called()
def test_drop_tag_lease(resource_manager): def test_drop_tag_lease(resource_manager):
resource_manager.redis.delete.return_value = True resource_manager.redis.delete.return_value = True
resource_manager.drop_tag_lease("tag_id") resource_manager.drop_tag_lease('tag_id')
resource_manager.redis.delete.assert_called_once_with( resource_manager.redis.delete.assert_called_once_with('lease:opc_tags:tag_id')
"lease:opc_tags:tag_id")
def test_get_all_ingestors(resource_manager): def test_get_all_ingestors(resource_manager):
resource_manager.redis.keys.return_value = ["ingestor1", "ingestor2"] resource_manager.redis.keys.return_value = ['ingestor1', 'ingestor2']
result = resource_manager.get_all_ingestors() result = resource_manager.get_all_ingestors()
assert result == ["ingestor1", "ingestor2"] assert result == ['ingestor1', 'ingestor2']
resource_manager.redis.keys.assert_called_once_with("heartbeat:ingestor:*") resource_manager.redis.keys.assert_called_once_with('heartbeat:ingestor:*')
def test_get_all_slots(resource_manager): def test_get_all_slots(resource_manager):
resource_manager.redis.keys.return_value = ["slot1", "slot2"] resource_manager.redis.keys.return_value = ['slot1', 'slot2']
result = resource_manager.get_all_slots() result = resource_manager.get_all_slots()
assert result == ["slot1", "slot2"] assert result == ['slot1', 'slot2']
resource_manager.redis.keys.assert_called_once_with("slot:opc_tags:*") resource_manager.redis.keys.assert_called_once_with('slot:opc_tags:*')
def test_get_all_leases(resource_manager): def test_get_all_leases(resource_manager):
resource_manager.redis.keys.return_value = ["lease1", "lease2"] resource_manager.redis.keys.return_value = ['lease1', 'lease2']
result = resource_manager.get_all_leases() result = resource_manager.get_all_leases()
assert result == ["lease1", "lease2"] assert result == ['lease1', 'lease2']
resource_manager.redis.keys.assert_called_once_with("lease:opc_tags:*") resource_manager.redis.keys.assert_called_once_with('lease:opc_tags:*')
def test_init_connection_failure(monkeypatch): def test_init_connection_failure(monkeypatch):
# Mock Redis to raise an exception during initialization # Mock Redis to raise an exception during initialization
mock_redis = MagicMock() mock_redis = MagicMock()
mock_redis.side_effect = Exception("Connection failed") mock_redis.side_effect = Exception('Connection failed')
monkeypatch.setattr("ingestor.managers.resource_manager.Redis", mock_redis) monkeypatch.setattr('ingestor.managers.resource_manager.Redis', mock_redis)
# Test that the exception is raised and metrics are set properly # Test that the exception is raised and metrics are set properly
with patch("ingestor.metrics.REDIS_CONNECTION_STATUS") as mock_metrics: with patch('ingestor.metrics.REDIS_CONNECTION_STATUS') as mock_metrics:
mock_status = MagicMock() mock_status = MagicMock()
mock_metrics.labels.return_value = mock_status mock_metrics.labels.return_value = mock_status
with raises(Exception, match="Connection failed"): with raises(Exception, match='Connection failed'):
ResourceManager( ResourceManager(
host="localhost", host='localhost',
port=6379, port=6379,
lease_ttl=10, lease_ttl=10,
heartbeat_ttl=10, heartbeat_ttl=10,
metadata=metadata["metadata"], metadata=metadata['metadata'],
logger=MagicMock(), logger=MagicMock(),
notification_handler=MagicMock(), notification_handler=MagicMock(),
) )
mock_metrics.labels.assert_called_once_with(pod_id="localhost") mock_metrics.labels.assert_called_once_with(pod_id='localhost')
mock_status.set.assert_called_once_with(0) mock_status.set.assert_called_once_with(0)
def test_init_ping_failure(monkeypatch): def test_init_ping_failure(monkeypatch):
# Mock Redis ping to raise an exception # Mock Redis ping to raise an exception
mock_redis_instance = MagicMock() mock_redis_instance = MagicMock()
mock_redis_instance.ping.side_effect = Exception("Ping failed") mock_redis_instance.ping.side_effect = Exception('Ping failed')
mock_redis_class = MagicMock(return_value=mock_redis_instance) mock_redis_class = MagicMock(return_value=mock_redis_instance)
monkeypatch.setattr( monkeypatch.setattr('ingestor.managers.resource_manager.Redis', mock_redis_class)
"ingestor.managers.resource_manager.Redis", mock_redis_class)
# Test that the exception is raised and metrics are set properly # Test that the exception is raised and metrics are set properly
with patch("ingestor.metrics.REDIS_CONNECTION_STATUS") as mock_metrics: with patch('ingestor.metrics.REDIS_CONNECTION_STATUS') as mock_metrics:
mock_status = MagicMock() mock_status = MagicMock()
mock_metrics.labels.return_value = mock_status mock_metrics.labels.return_value = mock_status
with raises(Exception, match="Ping failed"): with raises(Exception, match='Ping failed'):
ResourceManager( ResourceManager(
host="localhost", host='localhost',
port=6379, port=6379,
lease_ttl=10, lease_ttl=10,
heartbeat_ttl=10, heartbeat_ttl=10,
metadata=metadata["metadata"], metadata=metadata['metadata'],
logger=MagicMock(), logger=MagicMock(),
notification_handler=MagicMock(), notification_handler=MagicMock(),
) )
mock_metrics.labels.assert_called_once_with(pod_id="localhost") mock_metrics.labels.assert_called_once_with(pod_id='localhost')
mock_status.set.assert_called_once_with(0) mock_status.set.assert_called_once_with(0)
def test_execute_redis_op_success(resource_manager): def test_execute_redis_op_success(resource_manager):
# Mock the Redis operation and time function # Mock the Redis operation and time function
mock_func = MagicMock(return_value="test_result") mock_func = MagicMock(return_value='test_result')
with patch("ingestor.managers.resource_manager.time", side_effect=[100, 100.5]): with patch('ingestor.managers.resource_manager.time', side_effect=[100, 100.5]):
with patch("ingestor.metrics.REDIS_OPERATIONS_TOTAL") as mock_total: with patch('ingestor.metrics.REDIS_OPERATIONS_TOTAL') as mock_total:
with patch("ingestor.metrics.REDIS_OPERATIONS_DURATION") as mock_duration: with patch('ingestor.metrics.REDIS_OPERATIONS_DURATION') as mock_duration:
mock_total_labels = MagicMock() mock_total_labels = MagicMock()
mock_duration_labels = MagicMock() mock_duration_labels = MagicMock()
mock_total.labels.return_value = mock_total_labels mock_total.labels.return_value = mock_total_labels
@@ -187,49 +183,42 @@ def test_execute_redis_op_success(resource_manager):
# Execute the operation # Execute the operation
result = resource_manager._execute_redis_op( result = resource_manager._execute_redis_op(
"test_op", mock_func, "arg1", kwarg1="value1" 'test_op', mock_func, 'arg1', kwarg1='value1'
) )
# Verify the result and metrics # Verify the result and metrics
assert result == "test_result" assert result == 'test_result'
mock_func.assert_called_once_with("arg1", kwarg1="value1") mock_func.assert_called_once_with('arg1', kwarg1='value1')
mock_total.labels.assert_called_once_with( mock_total.labels.assert_called_once_with(pod_id='localhost', operation='test_op')
pod_id="localhost", operation="test_op"
)
mock_total_labels.inc.assert_called_once() mock_total_labels.inc.assert_called_once()
mock_duration.labels.assert_called_once_with( mock_duration.labels.assert_called_once_with(
pod_id="localhost", operation="test_op" pod_id='localhost', operation='test_op'
)
mock_duration_labels.observe.assert_called_once_with(
0.5
) )
mock_duration_labels.observe.assert_called_once_with(0.5)
def test_execute_redis_op_exception(resource_manager): def test_execute_redis_op_exception(resource_manager):
# Mock the Redis operation to raise an exception # Mock the Redis operation to raise an exception
mock_func = MagicMock(side_effect=Exception("Operation failed")) mock_func = MagicMock(side_effect=Exception('Operation failed'))
with patch("ingestor.managers.resource_manager.time", return_value=100): with patch('ingestor.managers.resource_manager.time', return_value=100):
with patch("ingestor.metrics.REDIS_OPERATIONS_ERRORS") as mock_errors: with patch('ingestor.metrics.REDIS_OPERATIONS_ERRORS') as mock_errors:
mock_errors_labels = MagicMock() mock_errors_labels = MagicMock()
mock_errors.labels.return_value = mock_errors_labels mock_errors.labels.return_value = mock_errors_labels
# Execute the operation and expect an exception # Execute the operation and expect an exception
with raises(Exception, match="Operation failed"): with raises(Exception, match='Operation failed'):
resource_manager._execute_redis_op( resource_manager._execute_redis_op('test_op', mock_func, 'arg1')
"test_op", mock_func, "arg1")
# Verify metrics and error handling # Verify metrics and error handling
mock_errors.labels.assert_called_once_with( mock_errors.labels.assert_called_once_with(pod_id='localhost', operation='test_op')
pod_id="localhost", operation="test_op"
)
mock_errors_labels.inc.assert_called_once() mock_errors_labels.inc.assert_called_once()
resource_manager.send_notification.assert_called_once_with( resource_manager.send_notification.assert_called_once_with(
metadata=metadata["metadata"], metadata=metadata['metadata'],
notification_id="REDIS_OPERATION_ERROR_test_op", notification_id='REDIS_OPERATION_ERROR_test_op',
message="Error in Redis operation 'test_op': Operation failed", message="Error in Redis operation 'test_op': Operation failed",
block="redis_manager", block='redis_manager',
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
) )

View File

@@ -1,91 +1,80 @@
import pytest
import pytest_asyncio
from unittest.mock import patch, MagicMock, call, AsyncMock
from threading import Event
import signal as signal_module # To avoid conflict with mock names
import os
import asyncio import asyncio
import signal as signal_module # To avoid conflict with mock names
from threading import Event
from unittest.mock import AsyncMock, MagicMock, call
import pytest
# Import the 'app' module to be tested # Import the 'app' module to be tested
from ingestor import app from ingestor import app
# Custom exception to catch os._exit calls # Custom exception to catch os._exit calls
class OsExitCalled(Exception): class OsExitCalledError(Exception):
def __init__(self, code): def __init__(self, code):
super().__init__(f"os._exit({code}) called") super().__init__(f'os._exit({code}) called')
self.code = code self.code = code
# Helper function for the os_exit mock's side_effect # Helper function for the os_exit mock's side_effect
def raise_os_exit_with_code(exit_code): def raise_os_exit_with_code(exit_code):
raise OsExitCalled(exit_code) raise OsExitCalledError(exit_code)
@pytest.fixture @pytest.fixture
def mock_app_env(monkeypatch): def mock_app_env(monkeypatch):
"""Fixture to mock dependencies of app.main and app.signal_handler.""" """Fixture to mock dependencies of app.main and app.signal_handler."""
mocks = { mocks = {
"start_http_server": MagicMock(), 'start_http_server': MagicMock(),
"Ingestor": MagicMock(), 'Ingestor': MagicMock(),
"metrics_APP_UP_labels_set": MagicMock(), 'metrics_APP_UP_labels_set': MagicMock(),
"metrics_APP_LOOP_COUNT_labels_inc": MagicMock(), 'metrics_APP_LOOP_COUNT_labels_inc': MagicMock(),
"metrics_APP_LOOP_DURATION_labels_observe": MagicMock(), 'metrics_APP_LOOP_DURATION_labels_observe': MagicMock(),
"metrics_APP_ERRORS_TOTAL_labels_inc": MagicMock(), 'metrics_APP_ERRORS_TOTAL_labels_inc': MagicMock(),
"os_exit": MagicMock(side_effect=raise_os_exit_with_code), 'os_exit': MagicMock(side_effect=raise_os_exit_with_code),
"time_time": MagicMock(), 'time_time': MagicMock(),
"asyncio_sleep": AsyncMock(), 'asyncio_sleep': AsyncMock(),
"signal_signal": MagicMock(), 'signal_signal': MagicMock(),
"traceback_print_exc": MagicMock(), 'traceback_print_exc': MagicMock(),
"mock_exit_signal": MagicMock(spec=Event), 'mock_exit_signal': MagicMock(spec=Event),
} }
monkeypatch.setattr(app, "start_http_server", mocks["start_http_server"]) monkeypatch.setattr(app, 'start_http_server', mocks['start_http_server'])
monkeypatch.setattr(app, "Ingestor", mocks["Ingestor"]) monkeypatch.setattr(app, 'Ingestor', mocks['Ingestor'])
monkeypatch.setattr(asyncio, "sleep", mocks["asyncio_sleep"]) monkeypatch.setattr(asyncio, 'sleep', mocks['asyncio_sleep'])
monkeypatch.setattr( monkeypatch.setattr(
app.metrics.APP_UP, app.metrics.APP_UP,
"labels", 'labels',
MagicMock(return_value=MagicMock( MagicMock(return_value=MagicMock(set=mocks['metrics_APP_UP_labels_set'])),
set=mocks["metrics_APP_UP_labels_set"])),
) )
monkeypatch.setattr( monkeypatch.setattr(
app.metrics.APP_LOOP_COUNT, app.metrics.APP_LOOP_COUNT,
"labels", 'labels',
MagicMock( MagicMock(return_value=MagicMock(inc=mocks['metrics_APP_LOOP_COUNT_labels_inc'])),
return_value=MagicMock(
inc=mocks["metrics_APP_LOOP_COUNT_labels_inc"])
),
) )
monkeypatch.setattr( monkeypatch.setattr(
app.metrics.APP_LOOP_DURATION, app.metrics.APP_LOOP_DURATION,
"labels", 'labels',
MagicMock( MagicMock(
return_value=MagicMock( return_value=MagicMock(observe=mocks['metrics_APP_LOOP_DURATION_labels_observe'])
observe=mocks["metrics_APP_LOOP_DURATION_labels_observe"]
)
), ),
) )
monkeypatch.setattr( monkeypatch.setattr(
app.metrics.APP_ERRORS_TOTAL, app.metrics.APP_ERRORS_TOTAL,
"labels", 'labels',
MagicMock( MagicMock(return_value=MagicMock(inc=mocks['metrics_APP_ERRORS_TOTAL_labels_inc'])),
return_value=MagicMock(
inc=mocks["metrics_APP_ERRORS_TOTAL_labels_inc"])
),
) )
monkeypatch.setattr(app.os, "_exit", mocks["os_exit"]) monkeypatch.setattr(app.os, '_exit', mocks['os_exit'])
monkeypatch.setattr(app, "time", mocks["time_time"]) monkeypatch.setattr(app, 'time', mocks['time_time'])
monkeypatch.setattr(app.signal, "signal", mocks["signal_signal"]) monkeypatch.setattr(app.signal, 'signal', mocks['signal_signal'])
monkeypatch.setattr(app.traceback, "print_exc", monkeypatch.setattr(app.traceback, 'print_exc', mocks['traceback_print_exc'])
mocks["traceback_print_exc"])
monkeypatch.setattr(app, "exit_signal", mocks["mock_exit_signal"]) monkeypatch.setattr(app, 'exit_signal', mocks['mock_exit_signal'])
monkeypatch.setattr(app, "POD_ID", "test_pod") monkeypatch.setattr(app, 'POD_ID', 'test_pod')
mock_ingestor_instance = mocks["Ingestor"].return_value mock_ingestor_instance = mocks['Ingestor'].return_value
mock_ingestor_instance.poll_interval = 0.01 mock_ingestor_instance.poll_interval = 0.01
mock_ingestor_instance.logger = MagicMock() mock_ingestor_instance.logger = MagicMock()
@@ -100,119 +89,112 @@ def mock_app_env(monkeypatch):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_successful_run_one_loop(mock_app_env, capsys): async def test_main_successful_run_one_loop(mock_app_env, capsys):
"""Test a successful run where the loop executes once and then exits gracefully.""" """Test a successful run where the loop executes once and then exits gracefully."""
mock_ingestor_instance = mock_app_env["Ingestor"].return_value mock_ingestor_instance = mock_app_env['Ingestor'].return_value
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_exit_signal.is_set.side_effect = [False, True] mock_exit_signal.is_set.side_effect = [False, True]
mock_app_env["time_time"].side_effect = [10.0, 11.5] mock_app_env['time_time'].side_effect = [10.0, 11.5]
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 0 assert excinfo.value.code == 0
mock_app_env["start_http_server"].assert_called_once_with(9090) mock_app_env['start_http_server'].assert_called_once_with(9090)
app.metrics.APP_UP.labels.assert_any_call(pod_id="test_pod") app.metrics.APP_UP.labels.assert_any_call(pod_id='test_pod')
set_calls = mock_app_env["metrics_APP_UP_labels_set"].call_args_list set_calls = mock_app_env['metrics_APP_UP_labels_set'].call_args_list
assert call(1) in set_calls assert call(1) in set_calls
assert call(0) in set_calls assert call(0) in set_calls
assert set_calls.index(call(1)) < set_calls.index(call(0)) assert set_calls.index(call(1)) < set_calls.index(call(0))
mock_app_env["Ingestor"].assert_called_once_with() mock_app_env['Ingestor'].assert_called_once_with()
mock_ingestor_instance.prepare_ingestor.assert_called_once() mock_ingestor_instance.prepare_ingestor.assert_called_once()
mock_ingestor_instance.logger.info.assert_any_call( mock_ingestor_instance.logger.info.assert_any_call('Ingestor prepared. Starting main loop.')
"Ingestor prepared. Starting main loop."
)
mock_ingestor_instance.loop.assert_called_once() mock_ingestor_instance.loop.assert_called_once()
app.metrics.APP_LOOP_COUNT.labels.assert_called_with(pod_id="test_pod") app.metrics.APP_LOOP_COUNT.labels.assert_called_with(pod_id='test_pod')
mock_app_env["metrics_APP_LOOP_COUNT_labels_inc"].assert_called_once() mock_app_env['metrics_APP_LOOP_COUNT_labels_inc'].assert_called_once()
mock_app_env["asyncio_sleep"].assert_has_calls( mock_app_env['asyncio_sleep'].assert_has_calls(
[call(mock_ingestor_instance.poll_interval), call(5)]) [call(mock_ingestor_instance.poll_interval), call(5)]
app.metrics.APP_LOOP_DURATION.labels.assert_called_with(pod_id="test_pod")
mock_app_env["metrics_APP_LOOP_DURATION_labels_observe"].assert_called_once_with(
1.5
) )
app.metrics.APP_LOOP_DURATION.labels.assert_called_with(pod_id='test_pod')
mock_app_env['metrics_APP_LOOP_DURATION_labels_observe'].assert_called_once_with(1.5)
mock_ingestor_instance.shutdown.assert_called_once() mock_ingestor_instance.shutdown.assert_called_once()
mock_ingestor_instance.logger.info.assert_any_call( mock_ingestor_instance.logger.info.assert_any_call('Main loop exit_signaled.')
"Main loop exit_signaled.")
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Prometheus server started on port 9090." in captured.out assert 'Prometheus server started on port 9090.' in captured.out
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_prometheus_server_fails_to_start(mock_app_env, capsys): async def test_main_prometheus_server_fails_to_start(mock_app_env, capsys):
"""Test the scenario where starting the Prometheus server fails.""" """Test the scenario where starting the Prometheus server fails."""
mock_app_env["start_http_server"].side_effect = OSError( mock_app_env['start_http_server'].side_effect = OSError('Port already in use')
"Port already in use")
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 1 assert excinfo.value.code == 1
# Check that .set(1) was not called. .set(0) definitely not called. # Check that .set(1) was not called. .set(0) definitely not called.
called_with_1 = False called_with_1 = False
for call_args in mock_app_env["metrics_APP_UP_labels_set"].call_args_list: for call_args in mock_app_env['metrics_APP_UP_labels_set'].call_args_list:
if call_args == call(1): if call_args == call(1):
called_with_1 = True called_with_1 = True
break break
assert ( assert not called_with_1, 'APP_UP.set(1) should not have been called if server start failed'
not called_with_1
), "APP_UP.set(1) should not have been called if server start failed"
mock_app_env["Ingestor"].assert_not_called() mock_app_env['Ingestor'].assert_not_called()
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Failed to start Prometheus server: Port already in use" in captured.out assert 'Failed to start Prometheus server: Port already in use' in captured.out
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_loop_exception_handling(mock_app_env, capsys): async def test_main_loop_exception_handling(mock_app_env, capsys):
"""Test that an exception in ingestor.loop() is handled gracefully.""" """Test that an exception in ingestor.loop() is handled gracefully."""
mock_ingestor_instance = mock_app_env["Ingestor"].return_value mock_ingestor_instance = mock_app_env['Ingestor'].return_value
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_exit_signal.is_set.side_effect = [False, True] mock_exit_signal.is_set.side_effect = [False, True]
mock_ingestor_instance.loop.side_effect = Exception("Test loop exception") mock_ingestor_instance.loop.side_effect = Exception('Test loop exception')
mock_app_env["time_time"].side_effect = [10.0, 10.1] mock_app_env['time_time'].side_effect = [10.0, 10.1]
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 0 assert excinfo.value.code == 0
mock_ingestor_instance.loop.assert_called_once() mock_ingestor_instance.loop.assert_called_once()
mock_app_env["traceback_print_exc"].assert_called_once() mock_app_env['traceback_print_exc'].assert_called_once()
app.metrics.APP_ERRORS_TOTAL.labels.assert_called_with(pod_id="test_pod") app.metrics.APP_ERRORS_TOTAL.labels.assert_called_with(pod_id='test_pod')
mock_app_env["metrics_APP_ERRORS_TOTAL_labels_inc"].assert_called_once() mock_app_env['metrics_APP_ERRORS_TOTAL_labels_inc'].assert_called_once()
mock_exit_signal.set.assert_called_once() mock_exit_signal.set.assert_called_once()
mock_app_env["metrics_APP_LOOP_COUNT_labels_inc"].assert_not_called() mock_app_env['metrics_APP_LOOP_COUNT_labels_inc'].assert_not_called()
app.metrics.APP_LOOP_DURATION.labels.assert_called_with(pod_id="test_pod") app.metrics.APP_LOOP_DURATION.labels.assert_called_with(pod_id='test_pod')
mock_app_env["metrics_APP_LOOP_DURATION_labels_observe"].assert_called_once_with( mock_app_env['metrics_APP_LOOP_DURATION_labels_observe'].assert_called_once_with(
pytest.approx(0.1) pytest.approx(0.1)
) )
mock_ingestor_instance.shutdown.assert_called_once() mock_ingestor_instance.shutdown.assert_called_once()
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Exception in main loop. Setting exit_signal flag." in captured.out assert 'Exception in main loop. Setting exit_signal flag.' in captured.out
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_keyboard_interrupt_handling(mock_app_env, capsys): async def test_main_keyboard_interrupt_handling(mock_app_env, capsys):
"""Test that KeyboardInterrupt in ingestor.loop() is handled.""" """Test that KeyboardInterrupt in ingestor.loop() is handled."""
mock_ingestor_instance = mock_app_env["Ingestor"].return_value mock_ingestor_instance = mock_app_env['Ingestor'].return_value
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_exit_signal.is_set.side_effect = [False, True] mock_exit_signal.is_set.side_effect = [False, True]
mock_ingestor_instance.loop.side_effect = KeyboardInterrupt() mock_ingestor_instance.loop.side_effect = KeyboardInterrupt()
mock_app_env["time_time"].side_effect = [10.0, 10.1] mock_app_env['time_time'].side_effect = [10.0, 10.1]
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 0 assert excinfo.value.code == 0
@@ -220,13 +202,13 @@ async def test_main_keyboard_interrupt_handling(mock_app_env, capsys):
mock_exit_signal.set.assert_called_once() mock_exit_signal.set.assert_called_once()
mock_ingestor_instance.shutdown.assert_called_once() mock_ingestor_instance.shutdown.assert_called_once()
captured = capsys.readouterr() captured = capsys.readouterr()
assert "KeyboardInterrupt received. Setting exit_signal flag." in captured.out assert 'KeyboardInterrupt received. Setting exit_signal flag.' in captured.out
mock_app_env["metrics_APP_ERRORS_TOTAL_labels_inc"].assert_not_called() mock_app_env['metrics_APP_ERRORS_TOTAL_labels_inc'].assert_not_called()
def test_signal_handler_sets_exit_signal(mock_app_env): def test_signal_handler_sets_exit_signal(mock_app_env):
"""Test that the signal_handler function calls exit_signal.set().""" """Test that the signal_handler function calls exit_signal.set()."""
mock_exit_signal_set = mock_app_env["mock_exit_signal"].set mock_exit_signal_set = mock_app_env['mock_exit_signal'].set
app.signal_handler(signal_module.SIGINT, None) app.signal_handler(signal_module.SIGINT, None)
mock_exit_signal_set.assert_called_once() mock_exit_signal_set.assert_called_once()
@@ -235,14 +217,13 @@ def test_signal_handler_sets_exit_signal(mock_app_env):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_multiple_loop_iterations(mock_app_env): async def test_main_multiple_loop_iterations(mock_app_env):
"""Test the main loop runs for a few iterations.""" """Test the main loop runs for a few iterations."""
mock_ingestor_instance = mock_app_env["Ingestor"].return_value mock_ingestor_instance = mock_app_env['Ingestor'].return_value
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_exit_signal.is_set.side_effect = [False, False, False, True] mock_exit_signal.is_set.side_effect = [False, False, False, True]
mock_app_env["time_time"].side_effect = [ mock_app_env['time_time'].side_effect = [10.0, 10.1, 10.2, 10.3, 10.4, 10.5]
10.0, 10.1, 10.2, 10.3, 10.4, 10.5]
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 0 assert excinfo.value.code == 0
@@ -250,17 +231,15 @@ async def test_main_multiple_loop_iterations(mock_app_env):
assert app.metrics.APP_LOOP_COUNT.labels.call_count == 3 assert app.metrics.APP_LOOP_COUNT.labels.call_count == 3
app.metrics.APP_LOOP_COUNT.labels.assert_called_with( app.metrics.APP_LOOP_COUNT.labels.assert_called_with(
pod_id="test_pod" pod_id='test_pod'
) # Checks last call or any call ) # Checks last call or any call
assert mock_app_env["metrics_APP_LOOP_COUNT_labels_inc"].call_count == 3 assert mock_app_env['metrics_APP_LOOP_COUNT_labels_inc'].call_count == 3
assert mock_app_env["asyncio_sleep"].call_count == 4 assert mock_app_env['asyncio_sleep'].call_count == 4
assert app.metrics.APP_LOOP_DURATION.labels.call_count == 3 assert app.metrics.APP_LOOP_DURATION.labels.call_count == 3
app.metrics.APP_LOOP_DURATION.labels.assert_called_with(pod_id="test_pod") app.metrics.APP_LOOP_DURATION.labels.assert_called_with(pod_id='test_pod')
duration_calls = mock_app_env[ duration_calls = mock_app_env['metrics_APP_LOOP_DURATION_labels_observe'].call_args_list
"metrics_APP_LOOP_DURATION_labels_observe"
].call_args_list
assert duration_calls[0] == call(pytest.approx(0.1, abs=1e-9)) assert duration_calls[0] == call(pytest.approx(0.1, abs=1e-9))
assert duration_calls[1] == call(pytest.approx(0.1, abs=1e-9)) assert duration_calls[1] == call(pytest.approx(0.1, abs=1e-9))
assert duration_calls[2] == call(pytest.approx(0.1, abs=1e-9)) assert duration_calls[2] == call(pytest.approx(0.1, abs=1e-9))
@@ -271,41 +250,41 @@ async def test_main_multiple_loop_iterations(mock_app_env):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_pod_id_used_in_metrics(mock_app_env): async def test_main_pod_id_used_in_metrics(mock_app_env):
"""Test that the POD_ID from app module is used in metric labels.""" """Test that the POD_ID from app module is used in metric labels."""
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_exit_signal.is_set.side_effect = [False, True] mock_exit_signal.is_set.side_effect = [False, True]
mock_app_env["time_time"].side_effect = [10.0, 11.0] mock_app_env['time_time'].side_effect = [10.0, 11.0]
with pytest.raises(OsExitCalled): with pytest.raises(OsExitCalledError):
await app.main() await app.main()
app.metrics.APP_UP.labels.assert_any_call(pod_id="test_pod") app.metrics.APP_UP.labels.assert_any_call(pod_id='test_pod')
app.metrics.APP_LOOP_COUNT.labels.assert_any_call(pod_id="test_pod") app.metrics.APP_LOOP_COUNT.labels.assert_any_call(pod_id='test_pod')
app.metrics.APP_LOOP_DURATION.labels.assert_any_call(pod_id="test_pod") app.metrics.APP_LOOP_DURATION.labels.assert_any_call(pod_id='test_pod')
# APP_ERRORS_TOTAL would be checked similarly if it were called in this flow. # APP_ERRORS_TOTAL would be checked similarly if it were called in this flow.
# Check the .set() / .inc() calls on the mocks returned by .labels() # Check the .set() / .inc() calls on the mocks returned by .labels()
mock_app_env["metrics_APP_UP_labels_set"].assert_any_call(1) mock_app_env['metrics_APP_UP_labels_set'].assert_any_call(1)
mock_app_env["metrics_APP_UP_labels_set"].assert_any_call(0) mock_app_env['metrics_APP_UP_labels_set'].assert_any_call(0)
mock_app_env["metrics_APP_LOOP_COUNT_labels_inc"].assert_called_once() mock_app_env['metrics_APP_LOOP_COUNT_labels_inc'].assert_called_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_async_main(mock_app_env, capsys): async def test_run_async_main(mock_app_env, capsys):
"""Test the run_async_main function that sets up the event loop.""" """Test the run_async_main function that sets up the event loop."""
mock_ingestor_instance = mock_app_env["Ingestor"].return_value mock_ingestor_instance = mock_app_env['Ingestor'].return_value
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_exit_signal.is_set.side_effect = [False, True] mock_exit_signal.is_set.side_effect = [False, True]
mock_app_env["time_time"].side_effect = [10.0, 11.0] mock_app_env['time_time'].side_effect = [10.0, 11.0]
# Instead of calling run_async_main() which creates a new event loop, # Instead of calling run_async_main() which creates a new event loop,
# we test the main() function directly since that's what run_async_main() would call # we test the main() function directly since that's what run_async_main() would call
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 0 assert excinfo.value.code == 0
# Verify the main function was called through the event loop # Verify the main function was called through the event loop
mock_app_env["start_http_server"].assert_called_once_with(9090) mock_app_env['start_http_server'].assert_called_once_with(9090)
mock_ingestor_instance.prepare_ingestor.assert_called_once() mock_ingestor_instance.prepare_ingestor.assert_called_once()
mock_ingestor_instance.loop.assert_called_once() mock_ingestor_instance.loop.assert_called_once()
mock_ingestor_instance.shutdown.assert_called_once() mock_ingestor_instance.shutdown.assert_called_once()
@@ -314,20 +293,19 @@ async def test_run_async_main(mock_app_env, capsys):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_prepare_ingestor_failure(mock_app_env, capsys): async def test_main_prepare_ingestor_failure(mock_app_env, capsys):
"""Test that prepare_ingestor failure is handled correctly.""" """Test that prepare_ingestor failure is handled correctly."""
mock_ingestor_instance = mock_app_env["Ingestor"].return_value mock_ingestor_instance = mock_app_env['Ingestor'].return_value
mock_exit_signal = mock_app_env["mock_exit_signal"] mock_exit_signal = mock_app_env['mock_exit_signal']
mock_ingestor_instance.prepare_ingestor.side_effect = Exception( mock_ingestor_instance.prepare_ingestor.side_effect = Exception('Preparation failed')
"Preparation failed")
mock_exit_signal.is_set.side_effect = [False, True] mock_exit_signal.is_set.side_effect = [False, True]
with pytest.raises(OsExitCalled) as excinfo: with pytest.raises(OsExitCalledError) as excinfo:
await app.main() await app.main()
assert excinfo.value.code == 0 assert excinfo.value.code == 0
# Verify error metrics were incremented # Verify error metrics were incremented
app.metrics.APP_ERRORS_TOTAL.labels.assert_called_with(pod_id="test_pod") app.metrics.APP_ERRORS_TOTAL.labels.assert_called_with(pod_id='test_pod')
mock_app_env["metrics_APP_ERRORS_TOTAL_labels_inc"].assert_called_once() mock_app_env['metrics_APP_ERRORS_TOTAL_labels_inc'].assert_called_once()
# Verify exit signal was set # Verify exit signal was set
mock_exit_signal.set.assert_called_once() mock_exit_signal.set.assert_called_once()
@@ -337,5 +315,5 @@ async def test_main_prepare_ingestor_failure(mock_app_env, capsys):
# Verify error was logged # Verify error was logged
mock_ingestor_instance.logger.error.assert_called_once_with( mock_ingestor_instance.logger.error.assert_called_once_with(
"Failed to prepare ingestor: Preparation failed" 'Failed to prepare ingestor: Preparation failed'
) )

View File

@@ -1,68 +1,70 @@
from unittest.mock import ANY, AsyncMock, MagicMock, patch, call from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
from pytest import fixture, mark from pytest import fixture, mark
from ingestor.ingestor import Ingestor from ingestor.ingestor import Ingestor
@patch("ingestor.ingestor.getenv") @patch('ingestor.ingestor.getenv')
@patch("ingestor.ingestor.NotificationHandler") @patch('ingestor.ingestor.NotificationHandler')
def test___init__(notification_handler, getenv): def test___init__(notification_handler, getenv):
getenv.side_effect = [ getenv.side_effect = [
"localhost:9092,localhost:35", # KAFKA_SERVERS 'localhost:9092,localhost:35', # KAFKA_SERVERS
"true", # EXPORT_TO_KAFKA 'true', # EXPORT_TO_KAFKA
"localhost1", # REDIS_HOST 'localhost1', # REDIS_HOST
'63790', # REDIS_PORT '63790', # REDIS_PORT
"user", # REDIS_USERNAME 'user', # REDIS_USERNAME
"password", # REDIS_PASSWORD 'password', # REDIS_PASSWORD
'100', # LEASE_TTL '100', # LEASE_TTL
'200', # HEARTBEAT_TTL '200', # HEARTBEAT_TTL
"localhost1", # HOSTNAME 'localhost1', # HOSTNAME
'50', # POLL_INTERVAL '50', # POLL_INTERVAL
"localhost:27017", # MONGODB_URL 'localhost:27017', # MONGODB_URL
"sientia", # MONGODB_USERNAME 'sientia', # MONGODB_USERNAME
"sientia", # MONGODB_PASSWORD 'sientia', # MONGODB_PASSWORD
"sientia" # MONGODB_DATABASE 'sientia', # MONGODB_DATABASE
] ]
ingestor = Ingestor() ingestor = Ingestor()
getenv.assert_any_call("KAFKA_SERVERS", "localhost:9092") getenv.assert_any_call('KAFKA_SERVERS', 'localhost:9092')
getenv.assert_any_call("REDIS_HOST", "localhost") getenv.assert_any_call('REDIS_HOST', 'localhost')
getenv.assert_any_call("REDIS_PORT", '6379') getenv.assert_any_call('REDIS_PORT', '6379')
getenv.assert_any_call("REDIS_USERNAME", None) getenv.assert_any_call('REDIS_USERNAME', None)
getenv.assert_any_call("REDIS_PASSWORD", None) getenv.assert_any_call('REDIS_PASSWORD', None)
getenv.assert_any_call("LEASE_TTL", '10') getenv.assert_any_call('LEASE_TTL', '10')
getenv.assert_any_call("HEARTBEAT_TTL", '20') getenv.assert_any_call('HEARTBEAT_TTL', '20')
getenv.assert_any_call("HOSTNAME", "localhost") getenv.assert_any_call('HOSTNAME', 'localhost')
getenv.assert_any_call("POLL_INTERVAL", '5') getenv.assert_any_call('POLL_INTERVAL', '5')
assert ingestor.kafka_servers == ["localhost:9092", "localhost:35"] assert ingestor.kafka_servers == ['localhost:9092', 'localhost:35']
assert ingestor.redis_host == "localhost1" assert ingestor.redis_host == 'localhost1'
assert ingestor.redis_port == 63790 assert ingestor.redis_port == 63790
assert ingestor.redis_username == "user" assert ingestor.redis_username == 'user'
assert ingestor.redis_password == "password" assert ingestor.redis_password == 'password'
assert ingestor.lease_ttl == 100 assert ingestor.lease_ttl == 100
assert ingestor.heartbeat_ttl == 200 assert ingestor.heartbeat_ttl == 200
assert ingestor.pod_id == "localhost1" assert ingestor.pod_id == 'localhost1'
assert ingestor.poll_interval == 50 assert ingestor.poll_interval == 50
assert ingestor.metadata == { assert ingestor.metadata == {
"model_id": "-", 'model_id': '-',
"model_name": "-", 'model_name': '-',
"workflow_name": "opc_ingestor", 'workflow_name': 'opc_ingestor',
"schema_name": "opc_ingestor", 'schema_name': 'opc_ingestor',
"pod_id": "localhost1" 'pod_id': 'localhost1',
} }
notification_handler.assert_called_once_with( notification_handler.assert_called_once_with(
connection_string="mongodb://sientia:sientia@localhost:27017", connection_string='mongodb://sientia:sientia@localhost:27017',
database="sientia", database='sientia',
logger=ingestor.logger, logger=ingestor.logger,
project_name="opc_ingestor" project_name='opc_ingestor',
) )
@fixture @fixture
@patch("ingestor.ingestor.getenv") @patch('ingestor.ingestor.getenv')
@patch("ingestor.ingestor.NotificationHandler") @patch('ingestor.ingestor.NotificationHandler')
def ingestor(_notification_handler, _getenv): def ingestor(_notification_handler, _getenv):
ing = Ingestor() ing = Ingestor()
ing.logger = MagicMock() ing.logger = MagicMock()
@@ -77,7 +79,7 @@ def ingestor_manager_started(ingestor):
shutdown=AsyncMock(), shutdown=AsyncMock(),
update_opc_servers=AsyncMock(), update_opc_servers=AsyncMock(),
subscribe_to_tags=AsyncMock(), subscribe_to_tags=AsyncMock(),
unsubscribe_slot=AsyncMock() unsubscribe_slot=AsyncMock(),
) )
return ingestor return ingestor
@@ -93,23 +95,23 @@ async def test_shutdown(ingestor_manager_started):
async def test_handle_acquired_tags_not_acquired(ingestor_manager_started): async def test_handle_acquired_tags_not_acquired(ingestor_manager_started):
await ingestor_manager_started.handle_acquired_tags([]) await ingestor_manager_started.handle_acquired_tags([])
ingestor_manager_started.logger.warning.assert_called_once_with( ingestor_manager_started.logger.warning.assert_called_once_with('No slots available')
"No slots available")
ingestor_manager_started.ingestor_manager.update_opc_servers.assert_not_called() ingestor_manager_started.ingestor_manager.update_opc_servers.assert_not_called()
ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_not_called() ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_not_called()
@mark.asyncio @mark.asyncio
async def test_handle_acquired_tags_success(ingestor_manager_started): async def test_handle_acquired_tags_success(ingestor_manager_started):
await ingestor_manager_started.handle_acquired_tags(["tag1", "tag2"]) await ingestor_manager_started.handle_acquired_tags(['tag1', 'tag2'])
ingestor_manager_started.logger.warning.assert_not_called() ingestor_manager_started.logger.warning.assert_not_called()
ingestor_manager_started.ingestor_manager.update_opc_servers.assert_called_once() ingestor_manager_started.ingestor_manager.update_opc_servers.assert_called_once()
ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_called_once_with( ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_called_once_with(
["tag1", "tag2"]) ['tag1', 'tag2']
)
@patch("ingestor.ingestor.IngestorManager") @patch('ingestor.ingestor.IngestorManager')
@mark.asyncio @mark.asyncio
async def test_prepare_ingestor(ingestor_manager_mock, ingestor): async def test_prepare_ingestor(ingestor_manager_mock, ingestor):
ingestor_manager = ingestor_manager_mock.return_value ingestor_manager = ingestor_manager_mock.return_value
@@ -120,12 +122,12 @@ async def test_prepare_ingestor(ingestor_manager_mock, ingestor):
await ingestor.prepare_ingestor() await ingestor.prepare_ingestor()
ingestor_manager_mock.assert_called_once_with( ingestor_manager_mock.assert_called_once_with(
kafka_servers=ingestor.kafka_servers, kafka_servers=','.join(ingestor.kafka_servers),
redis_data={ redis_data={
"host": ingestor.redis_host, 'host': ingestor.redis_host,
"port": ingestor.redis_port, 'port': ingestor.redis_port,
"username": ingestor.redis_username, 'username': ingestor.redis_username,
"password": ingestor.redis_password 'password': ingestor.redis_password,
}, },
lease_ttl=ingestor.lease_ttl, lease_ttl=ingestor.lease_ttl,
heartbeat_ttl=ingestor.heartbeat_ttl, heartbeat_ttl=ingestor.heartbeat_ttl,
@@ -141,7 +143,8 @@ async def test_prepare_ingestor(ingestor_manager_mock, ingestor):
ingestor_manager.get_slot_leases.assert_called_once() ingestor_manager.get_slot_leases.assert_called_once()
ingestor.handle_acquired_tags.assert_called_once_with( ingestor.handle_acquired_tags.assert_called_once_with(
ingestor_manager.get_slot_leases.return_value) ingestor_manager.get_slot_leases.return_value
)
def test_manage_slots_has_slots(ingestor_manager_started): def test_manage_slots_has_slots(ingestor_manager_started):
@@ -180,8 +183,14 @@ def test_manage_slots_none_available_none_available(ingestor_manager_started):
ingestor_manager_started.manage_no_slots(2) ingestor_manager_started.manage_no_slots(2)
ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with( ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with(1)
1)
@mark.asyncio
async def test_manage_leases_no_ingestor_manager(ingestor_manager_started):
ingestor_manager_started.ingestor_manager = None
assert await ingestor_manager_started.manage_leases(2, 2, 5) is None
@mark.asyncio @mark.asyncio
@@ -201,8 +210,7 @@ async def test_manage_leases_available_slots_innactive_ingestors(ingestor_manage
await ingestor_manager_started.manage_leases(2, 2, 5) await ingestor_manager_started.manage_leases(2, 2, 5)
ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with( ingestor_manager_started.ingestor_manager.get_slot_leases.assert_called_once_with(2)
2)
ingestor_manager_started.ingestor_manager.drop_slot_leases.assert_not_called() ingestor_manager_started.ingestor_manager.drop_slot_leases.assert_not_called()
@@ -211,9 +219,9 @@ async def test_manage_leases_available_slots_innactive_ingestors(ingestor_manage
async def test_manage_leases_no_available_slots_extra_sltos(ingestor_manager_started): async def test_manage_leases_no_available_slots_extra_sltos(ingestor_manager_started):
ingestor_manager_started.handle_acquired_tags = MagicMock() ingestor_manager_started.handle_acquired_tags = MagicMock()
ingestor_manager_started.ingestor_manager.managed_tags = { ingestor_manager_started.ingestor_manager.managed_tags = {
"tag1": "server1", 'tag1': 'server1',
"tag2": "server2", 'tag2': 'server2',
"tag3": "server3" 'tag3': 'server3',
} }
await ingestor_manager_started.manage_leases(0, 0, 2) await ingestor_manager_started.manage_leases(0, 0, 2)
@@ -221,7 +229,8 @@ async def test_manage_leases_no_available_slots_extra_sltos(ingestor_manager_sta
ingestor_manager_started.ingestor_manager.get_slot_leases.assert_not_called() ingestor_manager_started.ingestor_manager.get_slot_leases.assert_not_called()
ingestor_manager_started.handle_acquired_tags.assert_not_called() ingestor_manager_started.handle_acquired_tags.assert_not_called()
ingestor_manager_started.ingestor_manager.drop_slot_leases.assert_called_once_with( ingestor_manager_started.ingestor_manager.drop_slot_leases.assert_called_once_with(
["tag2", "tag3"]) ['tag2', 'tag3']
)
@mark.asyncio @mark.asyncio
@@ -230,16 +239,15 @@ async def test_loop(ingestor_manager_started):
ingestor_manager_started.manage_leases = AsyncMock() ingestor_manager_started.manage_leases = AsyncMock()
ingestor_manager_started.update_ingestor_manager = AsyncMock() ingestor_manager_started.update_ingestor_manager = AsyncMock()
ingestor_manager_started.ingestor_manager.managed_tags = { ingestor_manager_started.ingestor_manager.managed_tags = {
"slot1": "server1", 'slot1': 'server1',
"slot2": "server2", 'slot2': 'server2',
"slot3": "server3" 'slot3': 'server3',
} }
ingestor_manager_started.ingestor_manager.get_active_ingestors = MagicMock( ingestor_manager_started.ingestor_manager.get_active_ingestors = MagicMock(
return_value=["ingestor1", "ingestor2"]) return_value=['ingestor1', 'ingestor2']
ingestor_manager_started.ingestor_manager.get_number_of_slots = MagicMock( )
return_value=5) ingestor_manager_started.ingestor_manager.get_number_of_slots = MagicMock(return_value=5)
ingestor_manager_started.ingestor_manager.get_number_of_leases = MagicMock( ingestor_manager_started.ingestor_manager.get_number_of_leases = MagicMock(return_value=1)
return_value=1)
await ingestor_manager_started.loop() await ingestor_manager_started.loop()
@@ -248,10 +256,10 @@ async def test_loop(ingestor_manager_started):
ingestor_manager_started.ingestor_manager.get_number_of_slots.assert_called_once() ingestor_manager_started.ingestor_manager.get_number_of_slots.assert_called_once()
ingestor_manager_started.manage_no_slots.assert_called_once_with( ingestor_manager_started.manage_no_slots.assert_called_once_with(
ingestor_manager_started.ingestor_manager.get_number_of_slots.return_value) ingestor_manager_started.ingestor_manager.get_number_of_slots.return_value
)
# Explanation: 5 - 2 = 3, 3 - 1 = 2 # Explanation: 5 - 2 = 3, 3 - 1 = 2
ingestor_manager_started.manage_leases.assert_called_once_with( ingestor_manager_started.manage_leases.assert_called_once_with(4, 3, 2)
4, 3, 2)
ingestor_manager_started.ingestor_manager.update_slot_config.assert_called_once() ingestor_manager_started.ingestor_manager.update_slot_config.assert_called_once()
@@ -262,9 +270,9 @@ async def test_loop_no_managed(ingestor_manager_started):
ingestor_manager_started.update_ingestor_manager = AsyncMock() ingestor_manager_started.update_ingestor_manager = AsyncMock()
ingestor_manager_started.ingestor_manager.managed_tags = {} ingestor_manager_started.ingestor_manager.managed_tags = {}
ingestor_manager_started.ingestor_manager.get_active_ingestors = MagicMock( ingestor_manager_started.ingestor_manager.get_active_ingestors = MagicMock(
return_value=["ingestor1", "ingestor2"]) return_value=['ingestor1', 'ingestor2']
ingestor_manager_started.ingestor_manager.get_number_of_slots = MagicMock( )
return_value=5) ingestor_manager_started.ingestor_manager.get_number_of_slots = MagicMock(return_value=5)
await ingestor_manager_started.loop() await ingestor_manager_started.loop()
@@ -273,43 +281,48 @@ async def test_loop_no_managed(ingestor_manager_started):
ingestor_manager_started.ingestor_manager.get_number_of_slots.assert_called_once() ingestor_manager_started.ingestor_manager.get_number_of_slots.assert_called_once()
ingestor_manager_started.manage_no_slots.assert_called_once_with( ingestor_manager_started.manage_no_slots.assert_called_once_with(
ingestor_manager_started.ingestor_manager.get_number_of_slots.return_value) ingestor_manager_started.ingestor_manager.get_number_of_slots.return_value
)
# Explanation: 5 - 2 = 3, 3 - 1 = 2 # Explanation: 5 - 2 = 3, 3 - 1 = 2
ingestor_manager_started.manage_leases.assert_called_once_with( ingestor_manager_started.manage_leases.assert_called_once_with(ANY, 3, -1)
ANY, 3, -1)
ingestor_manager_started.ingestor_manager.update_slot_config.assert_called_once() ingestor_manager_started.ingestor_manager.update_slot_config.assert_called_once()
ingestor_manager_started.logger.info.assert_any_call( ingestor_manager_started.logger.info.assert_any_call('No slots acquired in this loop')
"No slots acquired in this loop")
@mark.asyncio
async def test_loop_no_ingestor_manager(ingestor_manager_started):
ingestor_manager_started.ingestor_manager = None
assert await ingestor_manager_started.loop() is None
@mark.asyncio @mark.asyncio
async def test_update_ingestor_manager(ingestor_manager_started): async def test_update_ingestor_manager(ingestor_manager_started):
ingestor_manager_started.ingestor_manager.managed_tags = { ingestor_manager_started.ingestor_manager.managed_tags = {
"slot_to_create": "new_config", 'slot_to_create': 'new_config',
"slot_to_update": "new_config", 'slot_to_update': 'new_config',
"slot_to_do_nothing": "old_config" 'slot_to_do_nothing': 'old_config',
} }
old_managed_tags = { old_managed_tags = {
"slot_to_update": "old_config", 'slot_to_update': 'old_config',
"slot_to_delete": "old_config", 'slot_to_delete': 'old_config',
"slot_to_do_nothing": "old_config" 'slot_to_do_nothing': 'old_config',
} }
await ingestor_manager_started.update_ingestor_manager(old_managed_tags) await ingestor_manager_started.update_ingestor_manager(old_managed_tags)
ingestor_manager_started.ingestor_manager.update_opc_servers.assert_called_once() ingestor_manager_started.ingestor_manager.update_opc_servers.assert_called_once()
ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_has_calls( ingestor_manager_started.ingestor_manager.subscribe_to_tags.assert_has_calls(
[ [call({'slot_to_create': 'new_config'}), call({'slot_to_update': 'new_config'})]
call(
{"slot_to_create": "new_config"}),
call(
{"slot_to_update": "new_config"})
]
) )
ingestor_manager_started.ingestor_manager.unsubscribe_slot.assert_has_calls( ingestor_manager_started.ingestor_manager.unsubscribe_slot.assert_has_calls(
[ [call('slot_to_update'), call('slot_to_delete')]
call("slot_to_update"),
call("slot_to_delete")
]
) )
@mark.asyncio
async def test_update_ingestor_manager_no_ingestor_manager(ingestor_manager_started):
ingestor_manager_started.ingestor_manager = None
await ingestor_manager_started.update_ingestor_manager({})

View File

@@ -1,7 +1,7 @@
# tests/unit/test_metrics.py # tests/unit/test_metrics.py
import pytest
from prometheus_client import Counter, Gauge, Histogram from prometheus_client import Counter, Gauge, Histogram
import ingestor.metrics as metrics import ingestor.metrics as metrics
# --- Test Functions for Each Metric (Corrected for v0.22.0 _name behavior) --- # --- Test Functions for Each Metric (Corrected for v0.22.0 _name behavior) ---
@@ -11,17 +11,16 @@ def test_ingestor_tag_written_count():
"""Verify the definition of INGESTOR_TAG_WRITTEN_COUNT.""" """Verify the definition of INGESTOR_TAG_WRITTEN_COUNT."""
assert metrics.TAG_WRITTEN_COUNT is not None assert metrics.TAG_WRITTEN_COUNT is not None
assert isinstance(metrics.TAG_WRITTEN_COUNT, Counter) assert isinstance(metrics.TAG_WRITTEN_COUNT, Counter)
assert metrics.TAG_WRITTEN_COUNT._name == "ingestor_tag_written_count" assert metrics.TAG_WRITTEN_COUNT._name == 'ingestor_tag_written_count'
assert set(metrics.TAG_WRITTEN_COUNT._labelnames) == { assert set(metrics.TAG_WRITTEN_COUNT._labelnames) == {'pod_id', 'tag_name', 'collection_name'}
"pod_id", "tag_name", "collection_name"}
def test_app_loop_count(): def test_app_loop_count():
"""Verify the definition of APP_LOOP_COUNT.""" """Verify the definition of APP_LOOP_COUNT."""
assert metrics.APP_LOOP_COUNT is not None assert metrics.APP_LOOP_COUNT is not None
assert isinstance(metrics.APP_LOOP_COUNT, Counter) assert isinstance(metrics.APP_LOOP_COUNT, Counter)
assert metrics.APP_LOOP_COUNT._name == "app_main_loop" # REMOVED _total assert metrics.APP_LOOP_COUNT._name == 'app_main_loop' # REMOVED _total
assert set(metrics.APP_LOOP_COUNT._labelnames) == {"pod_id"} assert set(metrics.APP_LOOP_COUNT._labelnames) == {'pod_id'}
def test_app_loop_duration(): def test_app_loop_duration():
@@ -29,32 +28,32 @@ def test_app_loop_duration():
assert metrics.APP_LOOP_DURATION is not None assert metrics.APP_LOOP_DURATION is not None
assert isinstance(metrics.APP_LOOP_DURATION, Histogram) assert isinstance(metrics.APP_LOOP_DURATION, Histogram)
assert ( assert (
metrics.APP_LOOP_DURATION._name == "app_main_loop_duration_seconds" metrics.APP_LOOP_DURATION._name == 'app_main_loop_duration_seconds'
) # Histograms don't have _total ) # Histograms don't have _total
assert set(metrics.APP_LOOP_DURATION._labelnames) == {"pod_id"} assert set(metrics.APP_LOOP_DURATION._labelnames) == {'pod_id'}
def test_app_errors_total(): def test_app_errors_total():
"""Verify the definition of APP_ERRORS_TOTAL.""" """Verify the definition of APP_ERRORS_TOTAL."""
assert metrics.APP_ERRORS_TOTAL is not None assert metrics.APP_ERRORS_TOTAL is not None
assert isinstance(metrics.APP_ERRORS_TOTAL, Counter) assert isinstance(metrics.APP_ERRORS_TOTAL, Counter)
assert metrics.APP_ERRORS_TOTAL._name == "app_errors" # REMOVED _total assert metrics.APP_ERRORS_TOTAL._name == 'app_errors' # REMOVED _total
assert set(metrics.APP_ERRORS_TOTAL._labelnames) == {"pod_id"} assert set(metrics.APP_ERRORS_TOTAL._labelnames) == {'pod_id'}
def test_app_up(): def test_app_up():
"""Verify the definition of APP_UP.""" """Verify the definition of APP_UP."""
assert metrics.APP_UP is not None assert metrics.APP_UP is not None
assert isinstance(metrics.APP_UP, Gauge) assert isinstance(metrics.APP_UP, Gauge)
assert metrics.APP_UP._name == "app_up" # Gauges don't have _total assert metrics.APP_UP._name == 'app_up' # Gauges don't have _total
assert set(metrics.APP_UP._labelnames) == {"pod_id"} assert set(metrics.APP_UP._labelnames) == {'pod_id'}
def test_active_ingestors(): def test_active_ingestors():
"""Verify the definition of ACTIVE_INGESTORS.""" """Verify the definition of ACTIVE_INGESTORS."""
assert metrics.ACTIVE_INGESTORS is not None assert metrics.ACTIVE_INGESTORS is not None
assert isinstance(metrics.ACTIVE_INGESTORS, Gauge) assert isinstance(metrics.ACTIVE_INGESTORS, Gauge)
assert metrics.ACTIVE_INGESTORS._name == "ingestor_active_total" assert metrics.ACTIVE_INGESTORS._name == 'ingestor_active_total'
assert set(metrics.ACTIVE_INGESTORS._labelnames) == set() assert set(metrics.ACTIVE_INGESTORS._labelnames) == set()
@@ -62,7 +61,7 @@ def test_slots_total():
"""Verify the definition of SLOTS_TOTAL.""" """Verify the definition of SLOTS_TOTAL."""
assert metrics.SLOTS_TOTAL is not None assert metrics.SLOTS_TOTAL is not None
assert isinstance(metrics.SLOTS_TOTAL, Gauge) assert isinstance(metrics.SLOTS_TOTAL, Gauge)
assert metrics.SLOTS_TOTAL._name == "ingestor_slots_total" assert metrics.SLOTS_TOTAL._name == 'ingestor_slots_total'
assert set(metrics.SLOTS_TOTAL._labelnames) == set() assert set(metrics.SLOTS_TOTAL._labelnames) == set()
@@ -70,7 +69,7 @@ def test_leases_total():
"""Verify the definition of LEASES_TOTAL.""" """Verify the definition of LEASES_TOTAL."""
assert metrics.LEASES_TOTAL is not None assert metrics.LEASES_TOTAL is not None
assert isinstance(metrics.LEASES_TOTAL, Gauge) assert isinstance(metrics.LEASES_TOTAL, Gauge)
assert metrics.LEASES_TOTAL._name == "ingestor_leases_total" assert metrics.LEASES_TOTAL._name == 'ingestor_leases_total'
assert set(metrics.LEASES_TOTAL._labelnames) == set() assert set(metrics.LEASES_TOTAL._labelnames) == set()
@@ -78,32 +77,32 @@ def test_slots_managed():
"""Verify the definition of SLOTS_MANAGED.""" """Verify the definition of SLOTS_MANAGED."""
assert metrics.SLOTS_MANAGED is not None assert metrics.SLOTS_MANAGED is not None
assert isinstance(metrics.SLOTS_MANAGED, Gauge) assert isinstance(metrics.SLOTS_MANAGED, Gauge)
assert metrics.SLOTS_MANAGED._name == "ingestor_slots_managed_current" assert metrics.SLOTS_MANAGED._name == 'ingestor_slots_managed_current'
assert set(metrics.SLOTS_MANAGED._labelnames) == {"pod_id"} assert set(metrics.SLOTS_MANAGED._labelnames) == {'pod_id'}
def test_slots_acquired(): def test_slots_acquired():
"""Verify the definition of SLOTS_ACQUIRED.""" """Verify the definition of SLOTS_ACQUIRED."""
assert metrics.SLOTS_ACQUIRED is not None assert metrics.SLOTS_ACQUIRED is not None
assert isinstance(metrics.SLOTS_ACQUIRED, Counter) assert isinstance(metrics.SLOTS_ACQUIRED, Counter)
assert metrics.SLOTS_ACQUIRED._name == "ingestor_slots_acquired" # REMOVED _total assert metrics.SLOTS_ACQUIRED._name == 'ingestor_slots_acquired' # REMOVED _total
assert set(metrics.SLOTS_ACQUIRED._labelnames) == {"pod_id"} assert set(metrics.SLOTS_ACQUIRED._labelnames) == {'pod_id'}
def test_slots_released(): def test_slots_released():
"""Verify the definition of SLOTS_RELEASED.""" """Verify the definition of SLOTS_RELEASED."""
assert metrics.SLOTS_RELEASED is not None assert metrics.SLOTS_RELEASED is not None
assert isinstance(metrics.SLOTS_RELEASED, Counter) assert isinstance(metrics.SLOTS_RELEASED, Counter)
assert metrics.SLOTS_RELEASED._name == "ingestor_slots_released" # REMOVED _total assert metrics.SLOTS_RELEASED._name == 'ingestor_slots_released' # REMOVED _total
assert set(metrics.SLOTS_RELEASED._labelnames) == {"pod_id"} assert set(metrics.SLOTS_RELEASED._labelnames) == {'pod_id'}
def test_opc_managers_active(): def test_opc_managers_active():
"""Verify the definition of OPC_MANAGERS_ACTIVE.""" """Verify the definition of OPC_MANAGERS_ACTIVE."""
assert metrics.OPC_MANAGERS_ACTIVE is not None assert metrics.OPC_MANAGERS_ACTIVE is not None
assert isinstance(metrics.OPC_MANAGERS_ACTIVE, Gauge) assert isinstance(metrics.OPC_MANAGERS_ACTIVE, Gauge)
assert metrics.OPC_MANAGERS_ACTIVE._name == "ingestor_opc_managers_active" assert metrics.OPC_MANAGERS_ACTIVE._name == 'ingestor_opc_managers_active'
assert set(metrics.OPC_MANAGERS_ACTIVE._labelnames) == {"pod_id"} assert set(metrics.OPC_MANAGERS_ACTIVE._labelnames) == {'pod_id'}
def test_opc_subscription_errors(): def test_opc_subscription_errors():
@@ -111,12 +110,12 @@ def test_opc_subscription_errors():
assert metrics.OPC_SUBSCRIPTION_ERRORS is not None assert metrics.OPC_SUBSCRIPTION_ERRORS is not None
assert isinstance(metrics.OPC_SUBSCRIPTION_ERRORS, Counter) assert isinstance(metrics.OPC_SUBSCRIPTION_ERRORS, Counter)
assert ( assert (
metrics.OPC_SUBSCRIPTION_ERRORS._name == "ingestor_opc_subscription_errors" metrics.OPC_SUBSCRIPTION_ERRORS._name == 'ingestor_opc_subscription_errors'
) # REMOVED _total ) # REMOVED _total
assert set(metrics.OPC_SUBSCRIPTION_ERRORS._labelnames) == { assert set(metrics.OPC_SUBSCRIPTION_ERRORS._labelnames) == {
"pod_id", 'pod_id',
"server", 'server',
"slot", 'slot',
} }
@@ -124,33 +123,27 @@ def test_opc_connections_total():
"""Verify the definition of OPC_CONNECTIONS_TOTAL.""" """Verify the definition of OPC_CONNECTIONS_TOTAL."""
assert metrics.OPC_CONNECTIONS_TOTAL is not None assert metrics.OPC_CONNECTIONS_TOTAL is not None
assert isinstance(metrics.OPC_CONNECTIONS_TOTAL, Counter) assert isinstance(metrics.OPC_CONNECTIONS_TOTAL, Counter)
assert ( assert metrics.OPC_CONNECTIONS_TOTAL._name == 'opc_connections_initiated' # REMOVED _total
metrics.OPC_CONNECTIONS_TOTAL._name == "opc_connections_initiated" assert set(metrics.OPC_CONNECTIONS_TOTAL._labelnames) == {'pod_id', 'server_name'}
) # REMOVED _total
assert set(metrics.OPC_CONNECTIONS_TOTAL._labelnames) == {
"pod_id", "server_name"}
def test_opc_connections_failed(): def test_opc_connections_failed():
"""Verify the definition of OPC_CONNECTIONS_FAILED.""" """Verify the definition of OPC_CONNECTIONS_FAILED."""
assert metrics.OPC_CONNECTIONS_FAILED is not None assert metrics.OPC_CONNECTIONS_FAILED is not None
assert isinstance(metrics.OPC_CONNECTIONS_FAILED, Counter) assert isinstance(metrics.OPC_CONNECTIONS_FAILED, Counter)
assert ( assert metrics.OPC_CONNECTIONS_FAILED._name == 'opc_connections_failed' # REMOVED _total
metrics.OPC_CONNECTIONS_FAILED._name == "opc_connections_failed" assert set(metrics.OPC_CONNECTIONS_FAILED._labelnames) == {'pod_id', 'server_name'}
) # REMOVED _total
assert set(metrics.OPC_CONNECTIONS_FAILED._labelnames) == {
"pod_id", "server_name"}
def test_opc_connection_status(): def test_opc_connection_status():
"""Verify the definition of OPC_CONNECTION_STATUS.""" """Verify the definition of OPC_CONNECTION_STATUS."""
assert metrics.OPC_CONNECTION_STATUS is not None assert metrics.OPC_CONNECTION_STATUS is not None
assert isinstance(metrics.OPC_CONNECTION_STATUS, Gauge) assert isinstance(metrics.OPC_CONNECTION_STATUS, Gauge)
assert metrics.OPC_CONNECTION_STATUS._name == "opc_connection_status" assert metrics.OPC_CONNECTION_STATUS._name == 'opc_connection_status'
assert set(metrics.OPC_CONNECTION_STATUS._labelnames) == { assert set(metrics.OPC_CONNECTION_STATUS._labelnames) == {
"pod_id", 'pod_id',
"server_name", 'server_name',
"server_url", 'server_url',
} }
@@ -158,13 +151,11 @@ def test_opc_subscriptions_created():
"""Verify the definition of OPC_SUBSCRIPTIONS_CREATED.""" """Verify the definition of OPC_SUBSCRIPTIONS_CREATED."""
assert metrics.OPC_SUBSCRIPTIONS_CREATED is not None assert metrics.OPC_SUBSCRIPTIONS_CREATED is not None
assert isinstance(metrics.OPC_SUBSCRIPTIONS_CREATED, Counter) assert isinstance(metrics.OPC_SUBSCRIPTIONS_CREATED, Counter)
assert ( assert metrics.OPC_SUBSCRIPTIONS_CREATED._name == 'opc_subscriptions_created' # REMOVED _total
metrics.OPC_SUBSCRIPTIONS_CREATED._name == "opc_subscriptions_created"
) # REMOVED _total
assert set(metrics.OPC_SUBSCRIPTIONS_CREATED._labelnames) == { assert set(metrics.OPC_SUBSCRIPTIONS_CREATED._labelnames) == {
"pod_id", 'pod_id',
"server_name", 'server_name',
"slot_name", 'slot_name',
} }
@@ -172,101 +163,85 @@ def test_opc_tags_subscribed():
"""Verify the definition of OPC_TAGS_SUBSCRIBED.""" """Verify the definition of OPC_TAGS_SUBSCRIBED."""
assert metrics.OPC_TAGS_SUBSCRIBED is not None assert metrics.OPC_TAGS_SUBSCRIBED is not None
assert isinstance(metrics.OPC_TAGS_SUBSCRIBED, Gauge) assert isinstance(metrics.OPC_TAGS_SUBSCRIBED, Gauge)
assert metrics.OPC_TAGS_SUBSCRIBED._name == "opc_tags_subscribed_current" assert metrics.OPC_TAGS_SUBSCRIBED._name == 'opc_tags_subscribed_current'
assert set(metrics.OPC_TAGS_SUBSCRIBED._labelnames) == { assert set(metrics.OPC_TAGS_SUBSCRIBED._labelnames) == {'pod_id', 'server_name'}
"pod_id", "server_name"}
def test_opc_cycles_without_data(): def test_opc_cycles_without_data():
"""Verify the definition of OPC_CYCLES_WITHOUT_DATA.""" """Verify the definition of OPC_CYCLES_WITHOUT_DATA."""
assert metrics.OPC_CYCLES_WITHOUT_DATA is not None assert metrics.OPC_CYCLES_WITHOUT_DATA is not None
assert isinstance(metrics.OPC_CYCLES_WITHOUT_DATA, Gauge) assert isinstance(metrics.OPC_CYCLES_WITHOUT_DATA, Gauge)
assert metrics.OPC_CYCLES_WITHOUT_DATA._name == "opc_cycles_without_data" assert metrics.OPC_CYCLES_WITHOUT_DATA._name == 'opc_cycles_without_data'
assert set(metrics.OPC_CYCLES_WITHOUT_DATA._labelnames) == { assert set(metrics.OPC_CYCLES_WITHOUT_DATA._labelnames) == {'pod_id', 'server_name'}
"pod_id", "server_name"}
def test_opc_reconnections_total(): def test_opc_reconnections_total():
"""Verify the definition of OPC_RECONNECTIONS_TOTAL.""" """Verify the definition of OPC_RECONNECTIONS_TOTAL."""
assert metrics.OPC_RECONNECTIONS_TOTAL is not None assert metrics.OPC_RECONNECTIONS_TOTAL is not None
assert isinstance(metrics.OPC_RECONNECTIONS_TOTAL, Counter) assert isinstance(metrics.OPC_RECONNECTIONS_TOTAL, Counter)
assert ( assert metrics.OPC_RECONNECTIONS_TOTAL._name == 'opc_reconnections_tried' # REMOVED _total
metrics.OPC_RECONNECTIONS_TOTAL._name == "opc_reconnections_tried" assert set(metrics.OPC_RECONNECTIONS_TOTAL._labelnames) == {'pod_id', 'server_name'}
) # REMOVED _total
assert set(metrics.OPC_RECONNECTIONS_TOTAL._labelnames) == {
"pod_id", "server_name"}
def test_kafka_messages_sent(): def test_kafka_messages_sent():
"""Verify the definition of KAFKA_MESSAGES_SENT.""" """Verify the definition of KAFKA_MESSAGES_SENT."""
assert metrics.KAFKA_MESSAGES_SENT is not None assert metrics.KAFKA_MESSAGES_SENT is not None
assert isinstance(metrics.KAFKA_MESSAGES_SENT, Counter) assert isinstance(metrics.KAFKA_MESSAGES_SENT, Counter)
assert metrics.KAFKA_MESSAGES_SENT._name == "kafka_messages_sent" # REMOVED _total assert metrics.KAFKA_MESSAGES_SENT._name == 'kafka_messages_sent' # REMOVED _total
assert set(metrics.KAFKA_MESSAGES_SENT._labelnames) == {"pod_id", "topic"} assert set(metrics.KAFKA_MESSAGES_SENT._labelnames) == {'pod_id', 'topic'}
def test_kafka_messages_errors(): def test_kafka_messages_errors():
"""Verify the definition of KAFKA_MESSAGES_ERRORS.""" """Verify the definition of KAFKA_MESSAGES_ERRORS."""
assert metrics.KAFKA_MESSAGES_ERRORS is not None assert metrics.KAFKA_MESSAGES_ERRORS is not None
assert isinstance(metrics.KAFKA_MESSAGES_ERRORS, Counter) assert isinstance(metrics.KAFKA_MESSAGES_ERRORS, Counter)
assert ( assert metrics.KAFKA_MESSAGES_ERRORS._name == 'kafka_messages_errors' # REMOVED _total
metrics.KAFKA_MESSAGES_ERRORS._name == "kafka_messages_errors" assert set(metrics.KAFKA_MESSAGES_ERRORS._labelnames) == {'pod_id', 'topic'}
) # REMOVED _total
assert set(metrics.KAFKA_MESSAGES_ERRORS._labelnames) == {
"pod_id", "topic"}
def test_kafka_connection_status(): def test_kafka_connection_status():
"""Verify the definition of KAFKA_CONNECTION_STATUS.""" """Verify the definition of KAFKA_CONNECTION_STATUS."""
assert metrics.KAFKA_CONNECTION_STATUS is not None assert metrics.KAFKA_CONNECTION_STATUS is not None
assert isinstance(metrics.KAFKA_CONNECTION_STATUS, Gauge) assert isinstance(metrics.KAFKA_CONNECTION_STATUS, Gauge)
assert metrics.KAFKA_CONNECTION_STATUS._name == "kafka_connection_status" assert metrics.KAFKA_CONNECTION_STATUS._name == 'kafka_connection_status'
assert set(metrics.KAFKA_CONNECTION_STATUS._labelnames) == {"pod_id"} assert set(metrics.KAFKA_CONNECTION_STATUS._labelnames) == {'pod_id'}
def test_redis_operations_total(): def test_redis_operations_total():
"""Verify the definition of REDIS_OPERATIONS_TOTAL.""" """Verify the definition of REDIS_OPERATIONS_TOTAL."""
assert metrics.REDIS_OPERATIONS_TOTAL is not None assert metrics.REDIS_OPERATIONS_TOTAL is not None
assert isinstance(metrics.REDIS_OPERATIONS_TOTAL, Counter) assert isinstance(metrics.REDIS_OPERATIONS_TOTAL, Counter)
assert metrics.REDIS_OPERATIONS_TOTAL._name == "redis_operations" # REMOVED _total assert metrics.REDIS_OPERATIONS_TOTAL._name == 'redis_operations' # REMOVED _total
assert set(metrics.REDIS_OPERATIONS_TOTAL._labelnames) == { assert set(metrics.REDIS_OPERATIONS_TOTAL._labelnames) == {'pod_id', 'operation'}
"pod_id", "operation"}
def test_redis_operations_errors(): def test_redis_operations_errors():
"""Verify the definition of REDIS_OPERATIONS_ERRORS.""" """Verify the definition of REDIS_OPERATIONS_ERRORS."""
assert metrics.REDIS_OPERATIONS_ERRORS is not None assert metrics.REDIS_OPERATIONS_ERRORS is not None
assert isinstance(metrics.REDIS_OPERATIONS_ERRORS, Counter) assert isinstance(metrics.REDIS_OPERATIONS_ERRORS, Counter)
assert ( assert metrics.REDIS_OPERATIONS_ERRORS._name == 'redis_operations_errors' # REMOVED _total
metrics.REDIS_OPERATIONS_ERRORS._name == "redis_operations_errors" assert set(metrics.REDIS_OPERATIONS_ERRORS._labelnames) == {'pod_id', 'operation'}
) # REMOVED _total
assert set(metrics.REDIS_OPERATIONS_ERRORS._labelnames) == {
"pod_id", "operation"}
def test_redis_operations_duration(): def test_redis_operations_duration():
"""Verify the definition of REDIS_OPERATIONS_DURATION.""" """Verify the definition of REDIS_OPERATIONS_DURATION."""
assert metrics.REDIS_OPERATIONS_DURATION is not None assert metrics.REDIS_OPERATIONS_DURATION is not None
assert isinstance(metrics.REDIS_OPERATIONS_DURATION, Histogram) assert isinstance(metrics.REDIS_OPERATIONS_DURATION, Histogram)
assert ( assert metrics.REDIS_OPERATIONS_DURATION._name == 'redis_operations_duration_seconds'
metrics.REDIS_OPERATIONS_DURATION._name == "redis_operations_duration_seconds" assert set(metrics.REDIS_OPERATIONS_DURATION._labelnames) == {'pod_id', 'operation'}
)
assert set(metrics.REDIS_OPERATIONS_DURATION._labelnames) == {
"pod_id", "operation"}
def test_redis_connection_status(): def test_redis_connection_status():
"""Verify the definition of REDIS_CONNECTION_STATUS.""" """Verify the definition of REDIS_CONNECTION_STATUS."""
assert metrics.REDIS_CONNECTION_STATUS is not None assert metrics.REDIS_CONNECTION_STATUS is not None
assert isinstance(metrics.REDIS_CONNECTION_STATUS, Gauge) assert isinstance(metrics.REDIS_CONNECTION_STATUS, Gauge)
assert metrics.REDIS_CONNECTION_STATUS._name == "redis_connection_status" assert metrics.REDIS_CONNECTION_STATUS._name == 'redis_connection_status'
assert set(metrics.REDIS_CONNECTION_STATUS._labelnames) == {"pod_id"} assert set(metrics.REDIS_CONNECTION_STATUS._labelnames) == {'pod_id'}
def test_notifications_sent(): def test_notifications_sent():
"""Verify the definition of NOTIFICATIONS_SENT.""" """Verify the definition of NOTIFICATIONS_SENT."""
assert metrics.NOTIFICATIONS_SENT is not None assert metrics.NOTIFICATIONS_SENT is not None
assert isinstance(metrics.NOTIFICATIONS_SENT, Counter) assert isinstance(metrics.NOTIFICATIONS_SENT, Counter)
assert metrics.NOTIFICATIONS_SENT._name == "notifications_sent" # REMOVED _total assert metrics.NOTIFICATIONS_SENT._name == 'notifications_sent' # REMOVED _total
assert set(metrics.NOTIFICATIONS_SENT._labelnames) == { assert set(metrics.NOTIFICATIONS_SENT._labelnames) == {'pod_id', 'level', 'block'}
"pod_id", "level", "block"}

124
validate.sh Executable file
View File

@@ -0,0 +1,124 @@
#!/bin/bash
# Model Manager Code Validation Script
# This script runs all code quality checks before committing or deploying
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Args
FIX_MODE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--fix)
FIX_MODE=true
shift
;;
-h|--help)
echo "Usage: $0 [--fix]"
echo " --fix Apply Ruff auto-fixes (format and lint fixes)."
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
echo "Usage: $0 [--fix]"
exit 2
;;
esac
done
echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Model Manager - Code Validation Suite ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
# Check if virtual environment is activated
if [[ -z "${VIRTUAL_ENV}" ]] && [[ -z "${CONDA_DEFAULT_ENV}" ]]; then
echo -e "${YELLOW}⚠️ Warning: No virtual environment detected${NC}"
echo -e "${YELLOW} Consider activating your venv/conda environment${NC}"
echo ""
fi
# Function to run a validation step
run_step() {
local step_name=$1
local step_command=$2
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE}${step_name}${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
if eval "$step_command"; then
echo -e "${GREEN}${step_name} - PASSED${NC}"
echo ""
return 0
else
echo -e "${RED}${step_name} - FAILED${NC}"
echo ""
return 1
fi
}
# Track failures
FAILED_STEPS=()
# Step 1: Code Formatting Check (Ruff)
# - default: check only
# - --fix: write changes
if ! run_step "1. Code Formatting (Ruff)" "if \$FIX_MODE; then ruff format ingestor/ tests/; else ruff format --check ingestor/ tests/; fi"; then
FAILED_STEPS+=("Code Formatting")
fi
# Step 2: Linting (Ruff)
# - default: check only
# - --fix: apply autofixes
if ! run_step "2. Code Linting (Ruff)" "if \$FIX_MODE; then ruff check --fix ingestor/ tests/; else ruff check ingestor/ tests/; fi"; then
FAILED_STEPS+=("Linting")
fi
# Step 3: Type Checking (mypy)
if ! run_step "3. Type Checking (mypy)" "mypy ingestor/"; then
FAILED_STEPS+=("Type Checking")
fi
# Step 4: Security Analysis (Bandit)
if ! run_step "4. Security Analysis (Bandit)" "bandit -r ingestor/ -ll -q"; then
FAILED_STEPS+=("Security Analysis")
fi
# Step 5: Unit Tests (pytest)
if ! run_step "5. Unit Tests (pytest)" "pytest tests/ --cov=ingestor --cov-report=term-missing --cov-report=xml --cov-report=html --cov-fail-under=80 -q"; then
FAILED_STEPS+=("Unit Tests")
fi
# Summary
echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Validation Summary ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
if [ ${#FAILED_STEPS[@]} -eq 0 ]; then
echo -e "${GREEN}✅ All validation checks passed!${NC}"
echo -e "${GREEN} Your code is ready for commit/deployment.${NC}"
echo ""
exit 0
else
echo -e "${RED}❌ Validation failed for the following steps:${NC}"
for step in "${FAILED_STEPS[@]}"; do
echo -e "${RED}${step}${NC}"
done
echo ""
echo -e "${YELLOW}💡 Tips:${NC}"
echo -e "${YELLOW} • Run 'ruff format ingestor/ tests/' to auto-fix formatting${NC}"
echo -e "${YELLOW} • Run 'ruff check --fix ingestor/ tests/' to auto-fix linting issues${NC}"
echo -e "${YELLOW} • Review mypy errors and add type hints where needed${NC}"
echo -e "${YELLOW} • Check bandit warnings for security issues${NC}"
echo -e "${YELLOW} • Fix failing tests or improve test coverage${NC}"
echo ""
exit 1
fi