-
Notifications
You must be signed in to change notification settings - Fork 0
Orm #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Orm #6
Changes from all commits
dc5104d
a3b2d09
e65a4cc
4dac63b
df669ba
a2e4dbd
0f65f5d
4874f44
e30fbe1
a265865
f3ca61b
4c3224f
61cb29a
2f67854
c7823b1
d5efa9f
4ebe96f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,3 +6,4 @@ pyyaml | |
| flask | ||
| ipdb | ||
| ipython | ||
| sqlalchemy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import os | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
|
|
||
| sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) | ||
|
|
||
| from sqlalchemy import create_engine, text | ||
|
|
||
| import ventis.controller.utils.sqlalchemy as sqlmod | ||
|
|
||
|
|
||
| class _FakeRedis: | ||
| def __init__(self, hashes): | ||
| self.hashes = hashes | ||
|
|
||
| def scan_keys(self, pattern): | ||
| prefix = pattern.rstrip("*") | ||
| return [k for k in self.hashes if k.startswith(prefix)] | ||
|
|
||
| def hgetall(self, name): | ||
| return dict(self.hashes.get(name, {})) | ||
|
|
||
| def get(self, name): | ||
| return self.hashes.get(name) | ||
|
|
||
|
|
||
| _CREATE = """ | ||
| CREATE TABLE runtime_information ( | ||
| future_id TEXT PRIMARY KEY, | ||
| session_id TEXT, | ||
| workflow TEXT, | ||
| agent TEXT, | ||
| execution_time REAL, | ||
| cpu_resource REAL, | ||
| gpu_resource REAL, | ||
| created_at TEXT, | ||
| updated_at TEXT | ||
| ) | ||
| """ | ||
|
|
||
|
|
||
| class RuntimeSqlalchemyTests(unittest.TestCase): | ||
| def setUp(self): | ||
| self.db = tempfile.NamedTemporaryFile(suffix=".db", delete=False) | ||
| self.db.close() | ||
| os.environ["VENTIS_DATABASE_URL"] = f"sqlite:///{self.db.name}" | ||
| sqlmod._engine = None | ||
| with create_engine(os.environ["VENTIS_DATABASE_URL"]).begin() as conn: | ||
| conn.execute(text(_CREATE)) | ||
|
|
||
| def tearDown(self): | ||
| sqlmod._engine = None | ||
| os.unlink(self.db.name) | ||
|
|
||
| def test_pull_and_upsert(self): | ||
| redis = _FakeRedis( | ||
| { | ||
| "future:abc": { | ||
| "id": "abc", | ||
| "request_id": "req1", | ||
| "agent": "AgentA", | ||
| "created_at": "1.0", | ||
| }, | ||
| "request:req1:workflow": "main", | ||
| "future:abc:consumers": {"x": "1"}, | ||
| } | ||
| ) | ||
| rows = sqlmod.pull_data(redis) | ||
| self.assertEqual(len(rows), 1) | ||
| self.assertEqual(rows[0]["future_id"], "abc") | ||
|
|
||
| sqlmod.send_data(rows, {"AgentA": {"cpu": 2, "gpu": 1}}, redis) | ||
| with sqlmod._get_engine("").connect() as conn: | ||
| row = conn.execute( | ||
| text( | ||
| "SELECT execution_time, cpu_resource, gpu_resource, workflow " | ||
| "FROM runtime_information WHERE future_id='abc'" | ||
| ) | ||
| ).fetchone() | ||
| self.assertGreaterEqual(row[0], 0) | ||
| self.assertEqual(row[1], 2.0) | ||
| self.assertEqual(row[2], 1.0) | ||
| self.assertEqual(row[3], "main") | ||
|
|
||
| rows[0]["finished_at"] = "9.0" | ||
| sqlmod.send_data(rows, {"AgentA": {"cpu": 2, "gpu": 1}}, redis) | ||
| with sqlmod._get_engine("").connect() as conn: | ||
| row = conn.execute( | ||
| text("SELECT * FROM runtime_information WHERE future_id='abc'") | ||
| ).fetchone() | ||
| self.assertEqual(row[4], 8.0) | ||
| self.assertEqual(row[8], "9.0") | ||
| for value in row: | ||
| self.assertNotIn(value, (None, "")) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| from ventis.controller.instance_manager import InstanceManager | ||
| from ventis.controller.utils.agent_specs import write_agent_specs | ||
| from ventis.controller.utils.redis_utils import _wait_for_redis | ||
| from ventis.controller.utils.sqlalchemy import pull_data, send_data | ||
| from ventis.utils.redis_client import RedisClient | ||
|
|
||
| # Add generated grpc_stubs from the local project to the path | ||
|
|
@@ -392,12 +393,21 @@ def run(self): | |
| self.stop() | ||
|
|
||
| def _poll_controllers(self): | ||
| """Check the health of each registered controller replica via its node's Redis.""" | ||
| """ | ||
| Check the health of each registered controller replica via its node's Redis. | ||
| Also retrieves the request calls made in each instance. | ||
| """ | ||
| for instance in self.instance_manager.list_instances(): | ||
| name = instance["agent_name"] | ||
| host = instance["host"] | ||
| port = instance["host_port"] | ||
| node_redis = self._get_node_redis_for(host) | ||
| send_data( | ||
| pull_data(node_redis), | ||
| {c["name"]: c.get("resources", {}) for c in self.controllers}, | ||
| node_redis, | ||
| self.config.get("database", {}).get("url"), | ||
| ) | ||
|
Comment on lines
+405
to
+410
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Do not clean up futures before persistence is acknowledged. The cleanup thread deletes 🤖 Prompt for AI Agents🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Keep controller health polling alive when persistence fails.
🤖 Prompt for AI Agents |
||
| agent_host = self._agent_host_key(host) | ||
| status_key = f"controller:{agent_host}:{port}:status" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,9 @@ def __init__(self, my_endpoint="unknown"): | |
| def Execute(self, request, context): | ||
| """Accept an Execute request and push it into the queue.""" | ||
| logger.info(f"Received request: {request.resonse}") | ||
| data = json.loads(request.resonse) | ||
| future_id = data.get("future_id") | ||
| self.redis.hset(f"future:{future_id}", "agent", data.get("service")) | ||
|
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Validate A malformed payload creates 🤖 Prompt for AI Agents |
||
| self.request_queue.put(request.resonse) | ||
| return local_controler_pb2.JsonResponse(resonse="Request queued successfully") | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| """Pull future hashes from Redis and upsert runtime_information rows.""" | ||
|
|
||
| import os | ||
| import time | ||
|
|
||
| from sqlalchemy import create_engine, text | ||
| from ventis.utils.redis_client import RedisClient | ||
|
|
||
| _engine = None | ||
|
|
||
| _UPSERT = text( | ||
| """ | ||
| INSERT INTO runtime_information ( | ||
| future_id, session_id, workflow, agent, execution_time, | ||
| cpu_resource, gpu_resource, created_at, updated_at | ||
| ) VALUES ( | ||
| :future_id, :session_id, :workflow, :agent, :execution_time, | ||
| :cpu_resource, :gpu_resource, :created_at, :updated_at | ||
| ) | ||
| ON CONFLICT(future_id) DO UPDATE SET | ||
| session_id=excluded.session_id, | ||
| workflow=excluded.workflow, | ||
| agent=excluded.agent, | ||
| execution_time=excluded.execution_time, | ||
| cpu_resource=excluded.cpu_resource, | ||
| gpu_resource=excluded.gpu_resource, | ||
| created_at=excluded.created_at, | ||
| updated_at=excluded.updated_at | ||
| """ | ||
| ) | ||
|
|
||
|
|
||
| def _get_engine(database_url): | ||
| global _engine | ||
| if _engine is None: | ||
| _engine = create_engine( | ||
| os.environ.get("VENTIS_DATABASE_URL", str(database_url)) | ||
| ) | ||
| return _engine | ||
|
|
||
|
|
||
| def pull_data(redis_client): | ||
| """Scan node Redis for future data""" | ||
| rows = [] | ||
| for key in redis_client.scan_keys("future:*"): | ||
| if key.count(":") != 1: | ||
| continue | ||
| data = redis_client.hgetall(key) | ||
| if data: | ||
| data["future_id"] = data.get("id") or key.split(":", 1)[1] | ||
| rows.append(data) | ||
| return rows | ||
|
|
||
|
|
||
| def send_data( | ||
| rows, | ||
| resources_by_agent=None, | ||
| redis_client: RedisClient | None = None, | ||
| database_url="", | ||
| ): | ||
| """UPSERT rows and attach allocated cpu/gpu from resources_by_agent.""" | ||
| if not rows: | ||
| return | ||
| resources_by_agent = resources_by_agent or {} | ||
| with _get_engine(database_url).begin() as conn: | ||
| for raw in rows: | ||
| agent = raw.get("agent") | ||
| res = resources_by_agent.get(agent, {}) | ||
| fid = raw.get("future_id") | ||
| if not fid: | ||
| continue | ||
| session_id = raw.get("request_id") | ||
| workflow = ( | ||
| redis_client.get(f"request:{session_id}:workflow") | ||
| if redis_client is not None | ||
| else None | ||
| ) | ||
| start = float(raw.get("created_at") or 0) | ||
| end = float(raw.get("finished_at") or time.time()) | ||
|
|
||
| conn.execute( | ||
| _UPSERT, | ||
| { | ||
| "future_id": fid, | ||
| "session_id": session_id, | ||
| "workflow": workflow, | ||
| "agent": agent, | ||
| "execution_time": end - start, | ||
| "cpu_resource": float(res.get("cpu", 0)), | ||
| "gpu_resource": float(res.get("gpu", 0)), | ||
| "created_at": str(start), | ||
| "updated_at": str(end), | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Restore
VENTIS_DATABASE_URLafter the test.The suite leaves the environment pointing to a deleted SQLite file, which can break tests that run afterward. Preserve the previous value in
setUp()and restore or remove it intearDown().🤖 Prompt for AI Agents