Backend: - service_auto module complet: router, service, schemas, 5 teste suites (22/22 passed) - 5 endpoints: GET /ping, /firme, /tip-deviz, /masini, POST /comenzi - SP_CREEAZA_COMANDA_PROTOTIP creat în MARIUSM_AUTO (VALID, 5.9ms) - oracle_pool.py: session_callback backward-compat patch - ROA_WEB user: grants SP-only confirmate (H3), mariusm_test pool switchat - pyproject.toml: integration pytest marker înregistrat Frontend: - ComandaNoua.vue: date reale din Oracle (firme/tip-deviz/masini), nu hardcodate - src/modules/service-auto/services/api.js: axios service cu Bearer token - src/router/index.js: rută /service-auto/comanda-noua Docs: - decision-log.md: verdict MERGE, toate 6 ipoteze CONFIRMED - learnings.md: 7 patterns reutilizabile - grants-audit.md: arhitectura multi-tenant + proxy auth analysis + V_NOM_FIRME loop - template-modul-oracle.md: rețetă completă pentru module Oracle noi - TODO-phase2.md: 7 items concrete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
"""
|
|
Integration test — Hypothesis #2: oracle pool concurrency — no state leak between connections.
|
|
|
|
Verifies that:
|
|
1. Two concurrent asyncio tasks can acquire connections from the same pool simultaneously
|
|
2. Each connection operates independently (no shared cursor state, no cross-connection leakage)
|
|
3. session_callback (if set) runs per-connection, not per-pool
|
|
|
|
Uses the 'mariusm_test' pool (ROA_WEB user). Requires live Oracle connection.
|
|
|
|
Run:
|
|
cd /workspace/roa2web
|
|
python -m pytest backend/modules/service_auto/tests/test_pool_concurrency.py -v -m integration
|
|
"""
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
|
|
|
|
from shared.database.oracle_pool import OracleMultiPool
|
|
|
|
SECRETS_FILE = os.path.join(
|
|
os.path.dirname(__file__), '..', '..', '..', 'secrets', 'mariusm_test.oracle_pass'
|
|
)
|
|
HOST = "10.0.20.121"
|
|
PORT = 1521
|
|
SERVICE_NAME = "ROA"
|
|
USER = "ROA_WEB"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pool():
|
|
"""Dedicated OracleMultiPool instance (not the global one) for isolation."""
|
|
with open(SECRETS_FILE) as f:
|
|
pwd = f.read().strip()
|
|
p = OracleMultiPool.__new__(OracleMultiPool)
|
|
p._pools = {}
|
|
p._pool_configs = {}
|
|
p._initialized = False
|
|
import asyncio as _asyncio
|
|
p._pool_lock = _asyncio.Lock()
|
|
p.register_server(
|
|
server_id="concurrency_test",
|
|
host=HOST, port=PORT,
|
|
user=USER, password=pwd,
|
|
service_name=SERVICE_NAME,
|
|
min_connections=2,
|
|
max_connections=5,
|
|
)
|
|
yield p
|
|
# Cleanup
|
|
import asyncio as _asyncio
|
|
_asyncio.get_event_loop().run_until_complete(p.close_pool())
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_two_concurrent_connections_return_correct_results(pool):
|
|
"""
|
|
Two asyncio tasks run simultaneously on the same pool.
|
|
Each uses a different bind value — results must not cross.
|
|
"""
|
|
results = {}
|
|
|
|
async def query_task(task_id: int, expected_val: int):
|
|
async with pool.get_connection("concurrency_test") as conn:
|
|
with conn.cursor() as cur:
|
|
# Short sleep to maximise overlap window
|
|
await asyncio.sleep(0.01)
|
|
cur.execute("SELECT :v FROM DUAL", {"v": expected_val})
|
|
row = cur.fetchone()
|
|
results[task_id] = row[0]
|
|
|
|
await asyncio.gather(
|
|
query_task(1, 111),
|
|
query_task(2, 222),
|
|
)
|
|
|
|
assert results[1] == 111, f"Task 1 expected 111, got {results[1]}"
|
|
assert results[2] == 222, f"Task 2 expected 222, got {results[2]}"
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_session_callback_runs_per_connection(pool):
|
|
"""
|
|
Register a session_callback that writes to a list.
|
|
Verify it fires each time a new connection is acquired.
|
|
session_callback must NOT bleed state across connections.
|
|
"""
|
|
callback_log = []
|
|
|
|
def schema_callback(connection, requested_tag):
|
|
"""Simulates ALTER SESSION SET CURRENT_SCHEMA; logs invocation."""
|
|
callback_log.append(id(connection))
|
|
|
|
# Register a second server config with session_callback
|
|
with open(SECRETS_FILE) as f:
|
|
pwd = f.read().strip()
|
|
|
|
pool.register_server(
|
|
server_id="cb_test",
|
|
host=HOST, port=PORT,
|
|
user=USER, password=pwd,
|
|
service_name=SERVICE_NAME,
|
|
min_connections=1,
|
|
max_connections=3,
|
|
session_callback=schema_callback,
|
|
)
|
|
|
|
# Acquire two connections sequentially (pool min=1 so first reuses, second may create)
|
|
conn_ids = []
|
|
async with pool.get_connection("cb_test") as conn:
|
|
conn_ids.append(id(conn))
|
|
async with pool.get_connection("cb_test") as conn:
|
|
conn_ids.append(id(conn))
|
|
|
|
# Callback must have fired at least once (at pool creation / first acquire)
|
|
assert len(callback_log) >= 1, (
|
|
"session_callback never called — pool did not invoke it on connection creation"
|
|
)
|
|
|
|
await pool.close_pool("cb_test")
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.asyncio
|
|
async def test_ten_concurrent_queries_no_errors(pool):
|
|
"""
|
|
Stress: 10 concurrent queries on pool with max_connections=5.
|
|
All must complete without errors (pool queues excess requests via POOL_GETMODE_WAIT).
|
|
"""
|
|
errors = []
|
|
|
|
async def single_query(i: int):
|
|
try:
|
|
async with pool.get_connection("concurrency_test") as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute("SELECT :i FROM DUAL", {"i": i})
|
|
val = cur.fetchone()[0]
|
|
assert val == i, f"Expected {i}, got {val}"
|
|
except Exception as e:
|
|
errors.append(f"task {i}: {e}")
|
|
|
|
t0 = time.perf_counter()
|
|
await asyncio.gather(*[single_query(i) for i in range(10)])
|
|
elapsed = time.perf_counter() - t0
|
|
|
|
assert not errors, f"Errors in concurrent queries: {errors}"
|
|
print(f"\n[CONCURRENCY] 10 queries completed in {elapsed*1000:.0f}ms, no errors ✅")
|