Skip to content

feature: added threading to ventis deploy | bug fix: fixed schema misalignment for runtime_information table#16

Merged
iidsample merged 2 commits into
mainfrom
#10-Threading-Deploy
Jul 21, 2026
Merged

feature: added threading to ventis deploy | bug fix: fixed schema misalignment for runtime_information table#16
iidsample merged 2 commits into
mainfrom
#10-Threading-Deploy

Conversation

@Saaketh0

@Saaketh0 Saaketh0 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Parallelize agent instance provisioning in ventis deploy

This PR replaces the sequential per-replica provisioning loop in InstanceManager.ensure_instances() with a threaded implementation. Each replica's provision_instance/bootstrap_instance call (a blocking docker run locally, or an EC2 launch/SSH/health-check sequence) is I/O-bound, so a ThreadPoolExecutor (sized via VENTIS_MAX_AGENT_INSTANCES, matching the existing pattern in local_controller.py) now runs replicas concurrently instead of one at a time, cutting deploy time roughly to the slowest single replica instead of the sum of all of them.

To avoid two local replicas racing to the same port, port assignment happens sequentially during a planning pass before any threads start, with each reservation persisted to Redis immediately (_next_host_port) so later replicas see it as taken. The existing-instance check now requires a runtime_id to treat a record as complete, so a replica left half-provisioned by a failed bootstrap is correctly retried rather than skipped.

Also included: Postgres support for the runtime-tracking ORM (psycopg driver, a TABLE_NAME constant, schema-compatibility fixes for execution_time/created_at/updated_at as floats, and skipping rows without a valid session), a small EC2 image-transfer robustness fix (pipefail + logging), and updated/added tests covering the new threading and DB behavior.

Summary by CodeRabbit

  • New Features
    • Added support for PostgreSQL connectivity.
    • Local runtime instances can now be provisioned concurrently, with automatic host-port reservation.
  • Bug Fixes
    • Improved runtime timing and resource data storage using numeric values for more reliable processing.
    • Enhanced EC2 image transfer handling and diagnostic logging.
  • Tests
    • Updated runtime provisioning and persistence tests to validate port allocation, callbacks, and numeric timestamps.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3927227d-f70f-4492-a251-0b6bb067f0eb

📥 Commits

Reviewing files that changed from the base of the PR and between 26141ff and a1a8c1f.

📒 Files selected for processing (1)
  • tests/test_runtime_sqlalchemy.py

📝 Walkthrough

Walkthrough

Instance provisioning now handles missing replicas concurrently, reserves local ports through Redis, and preserves existing instances. Runtime telemetry timestamps use numeric storage, PostgreSQL dependencies are declared, and EC2 image transfers now include Bash pipefail execution and logging.

Changes

Runtime operations

Layer / File(s) Summary
Concurrent instance provisioning
ventis/controller/instance_manager.py, tests/test_instance_manager_runtime.py
Missing replicas are provisioned concurrently, local ports are reserved in Redis, existing instances are retained, and provider-specific callback behavior is tested.
Runtime telemetry persistence
ventis/controller/utils/sqlalchemy.py, tests/test_runtime_sqlalchemy.py, pyproject.toml, requirements.txt
Runtime information upserts validate inputs and persist timestamps and resource values as floats; SQLite expectations and PostgreSQL dependency declarations are updated.
EC2 image-transfer diagnostics
ventis/controller/cloud_provider_logic/EC2/_runtime.py
Remote image transfer uses Bash with pipefail and logs the command result and captured output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InstanceManager
  participant Redis
  participant ThreadPoolExecutor
  participant Runtime
  participant Controller
  InstanceManager->>Redis: reserve local host port
  InstanceManager->>ThreadPoolExecutor: submit missing replica jobs
  ThreadPoolExecutor->>Runtime: provision replica with port callback
  Runtime-->>ThreadPoolExecutor: return provisioned instance
  ThreadPoolExecutor-->>InstanceManager: return completed jobs
  InstanceManager->>Controller: register instances and runtime IDs
  InstanceManager->>Redis: publish routing snapshot
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the two main changes: threaded deployment and runtime_information schema fixes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch #10-Threading-Deploy

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ventis/controller/instance_manager.py (1)

178-198: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Partial reservation record persisted under the final instance key can break downstream consumers that assume a complete record.

_next_host_port now hset_multiples agent_name/provider/replica_index/host/host_port directly into the same Redis key that _write_instance will later fully populate. If the job subsequently fails (or hangs) before _write_instance runs, this half-written record (no runtime_id, endpoint, container_port, etc.) is left behind and is fully visible to list_instances(), publish_routing_snapshot(), and remove_instance() — none of which check for runtime_id presence. In particular, remove_instance at Line 145/148 (instance['agent_name'], instance["runtime_id"]) and _destroy_runtime will raise KeyError/fail if invoked against such a partial record, since only ensure_instances was updated to treat missing runtime_id as "not yet provisioned."

🔧 Proposed guard
 def remove_instance(self, instance_id):
     key = f"agent_instance:{instance_id}"
     instance = self.redis.hgetall(key)
-    if not instance:
+    if not instance or not instance.get("runtime_id"):
         return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ventis/controller/instance_manager.py` around lines 178 - 198, Update
_next_host_port so port reservation does not persist a partial instance record
under the final key used by _write_instance. Store the reservation separately or
defer writing instance fields until _write_instance can populate the complete
record, while preserving unique host-port allocation and ensuring
list_instances, publish_routing_snapshot, and remove_instance only see fully
provisioned records.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ventis/controller/cloud_provider_logic/EC2/_runtime.py`:
- Around line 234-236: Update the SSH command in the image transfer flow to
remove StrictHostKeyChecking=no and enable strict host-key verification.
Configure and reference a managed known_hosts file or entry for the target host,
preserving the existing key-based authentication and docker save/load pipeline.
- Around line 242-244: Update the logging around the docker save/load result to
sanitize and size-limit result.stdout and result.stderr before INFO logging,
escaping multiline content and truncating oversized output. Preserve complete
transfer output only when the explicitly enabled diagnostic logging level is
active.
- Line 231: Update the logging around the image transfer in the relevant EC2
runtime flow to include structured, redacted SSH command details, including the
destination and options, while explicitly omitting the private-key path.
Preserve the existing image and host context in the log message and ensure
sensitive command arguments are not emitted.
- Around line 232-241: Add a configured timeout to the image-transfer
subprocess.run call in the image-transfer flow, and catch
subprocess.TimeoutExpired to convert the timeout into the same failure path used
for other transfer errors so instance cleanup still executes. Preserve the
existing command and successful transfer behavior.

In `@ventis/controller/instance_manager.py`:
- Around line 74-91: Update the concurrent provisioning flow around
_provision_one and future.result() to isolate failures per job: apply the
existing provisioning timeout policy, catch and record/log each future’s
exception without aborting ensure_instances, and continue aggregating successful
instances so _add_instance_to_agent, _track_runtime, and the later routing
snapshot still run for completed jobs.

---

Outside diff comments:
In `@ventis/controller/instance_manager.py`:
- Around line 178-198: Update _next_host_port so port reservation does not
persist a partial instance record under the final key used by _write_instance.
Store the reservation separately or defer writing instance fields until
_write_instance can populate the complete record, while preserving unique
host-port allocation and ensuring list_instances, publish_routing_snapshot, and
remove_instance only see fully provisioned records.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: add3b464-d4d9-4942-8682-ff43a15bf541

📥 Commits

Reviewing files that changed from the base of the PR and between 47cc57b and 26141ff.

📒 Files selected for processing (8)
  • pyproject.toml
  • requirements.txt
  • tests/test_instance_manager_runtime.py
  • tests/test_runtime_sqlalchemy.py
  • ventis/controller/cloud_provider_logic/EC2/_runtime.py
  • ventis/controller/instance_manager.py
  • ventis/controller/local_controller.py
  • ventis/controller/utils/sqlalchemy.py

if spec.get("type") == "workflow":
port_args += ["-p", f"{spec.get('api_port', 8080)}:8080"]

logger.info("Transferring image %s to %s", image, host)

Copy link
Copy Markdown

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

Log a redacted transfer command if command-level diagnostics are required.

This message records only the image and host, not the actual SSH destination/options. Add structured, redacted command details while omitting the private-key path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ventis/controller/cloud_provider_logic/EC2/_runtime.py` at line 231, Update
the logging around the image transfer in the relevant EC2 runtime flow to
include structured, redacted SSH command details, including the destination and
options, while explicitly omitting the private-key path. Preserve the existing
image and host context in the log message and ensure sensitive command arguments
are not emitted.

Comment on lines 232 to +241
result = subprocess.run(
"set -o pipefail; "
f"docker save {shlex.quote(image)} | ssh -o StrictHostKeyChecking=no "
f"-o IdentitiesOnly=yes -i {shlex.quote(key)} "
f"{shlex.quote(f'{ssh_user}@{host}')} 'sudo docker load'",
shell=True,
capture_output=True,
text=True,
executable="/bin/bash",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the image-transfer subprocess.

A stalled SSH handshake, sudo docker load, or pipeline can block a ThreadPoolExecutor worker indefinitely and prevent provisioning from completing. Pass a configured timeout and convert subprocess.TimeoutExpired into a failure so the existing instance-cleanup path runs.

🧰 Tools
🪛 OpenGrep (1.25.0)

[ERROR] 232-241: Dynamic command passed to subprocess with shell=True. Use a command list without shell=True, or use shlex.quote() to sanitize input.

(coderabbit.command-injection.python-shell-true)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ventis/controller/cloud_provider_logic/EC2/_runtime.py` around lines 232 -
241, Add a configured timeout to the image-transfer subprocess.run call in the
image-transfer flow, and catch subprocess.TimeoutExpired to convert the timeout
into the same failure path used for other transfer errors so instance cleanup
still executes. Preserve the existing command and successful transfer behavior.

Comment on lines 234 to 236
f"docker save {shlex.quote(image)} | ssh -o StrictHostKeyChecking=no "
f"-o IdentitiesOnly=yes -i {shlex.quote(key)} "
f"{shlex.quote(f'{ssh_user}@{host}')} 'sudo docker load'",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Restore SSH host-key verification.

StrictHostKeyChecking=no accepts any server host key, so the image can be transferred to an unintended endpoint without cryptographic host authentication. Configure a managed known_hosts entry and use strict verification for this provisioning path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ventis/controller/cloud_provider_logic/EC2/_runtime.py` around lines 234 -
236, Update the SSH command in the image transfer flow to remove
StrictHostKeyChecking=no and enable strict host-key verification. Configure and
reference a managed known_hosts file or entry for the target host, preserving
the existing key-based authentication and docker save/load pipeline.

Comment on lines +242 to +244
logger.info(
"docker save|load returncode=%s stdout=%s stderr=%s",
result.returncode, result.stdout, result.stderr,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Bound and sanitize transfer output before logging.

Remote stdout and stderr are logged verbatim at INFO without a size limit, allowing large or multiline output to flood or obscure logs. Truncate and escape the captured output, while retaining full details only at an explicitly enabled diagnostic level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ventis/controller/cloud_provider_logic/EC2/_runtime.py` around lines 242 -
244, Update the logging around the docker save/load result to sanitize and
size-limit result.stdout and result.stderr before INFO logging, escaping
multiline content and truncating oversized output. Preserve complete transfer
output only when the explicitly enabled diagnostic logging level is active.

Comment on lines +74 to +91
max_workers = int(os.environ.get("VENTIS_MAX_AGENT_INSTANCES", 8))
provisioned = []
if jobs:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_job = {
executor.submit(self._provision_one, job): job for job in jobs
}
for future in as_completed(future_to_job):
job = future_to_job[future]
instance = future.result()
provisioned.append(
(job["agent_name"], job["instance_id"], instance)
)
self._write_instance(instance)

self._add_instance_to_agent(agent_name, instance_id)
self._track_runtime(agent_name, instance["runtime_id"])
instances.append(instance)
for agent_name, instance_id, instance in existing + provisioned:
self._add_instance_to_agent(agent_name, instance_id)
self._track_runtime(agent_name, instance["runtime_id"])
instances.append(instance)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled/timeout-less future.result() aborts the whole batch on a single failure.

future.result() is called with no timeout and no per-future exception handling. If any one job (a slow EC2 boot, SSH hang, or docker failure) raises or blocks, it either hangs ensure_instances indefinitely or propagates immediately and skips _add_instance_to_agent/_track_runtime/publish_routing_snapshot for every other job that already completed successfully in this call (their Redis records are written via _write_instance inside _provision_one, but the aggregation/registration step at Line 88 is never reached). Callers get an unhandled exception instead of a partial result, and the routing snapshot is never republished even though other replicas came up fine.

🔧 Proposed fix: isolate failures per job
                 for future in as_completed(future_to_job):
                     job = future_to_job[future]
-                    instance = future.result()
-                    provisioned.append(
-                        (job["agent_name"], job["instance_id"], instance)
-                    )
+                    try:
+                        instance = future.result()
+                    except Exception:
+                        logger.exception(
+                            "Provisioning failed for %s replica %s",
+                            job["agent_name"], job["replica_index"],
+                        )
+                        continue
+                    provisioned.append(
+                        (job["agent_name"], job["instance_id"], instance)
+                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
max_workers = int(os.environ.get("VENTIS_MAX_AGENT_INSTANCES", 8))
provisioned = []
if jobs:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_job = {
executor.submit(self._provision_one, job): job for job in jobs
}
for future in as_completed(future_to_job):
job = future_to_job[future]
instance = future.result()
provisioned.append(
(job["agent_name"], job["instance_id"], instance)
)
self._write_instance(instance)
self._add_instance_to_agent(agent_name, instance_id)
self._track_runtime(agent_name, instance["runtime_id"])
instances.append(instance)
for agent_name, instance_id, instance in existing + provisioned:
self._add_instance_to_agent(agent_name, instance_id)
self._track_runtime(agent_name, instance["runtime_id"])
instances.append(instance)
max_workers = int(os.environ.get("VENTIS_MAX_AGENT_INSTANCES", 8))
provisioned = []
if jobs:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_job = {
executor.submit(self._provision_one, job): job for job in jobs
}
for future in as_completed(future_to_job):
job = future_to_job[future]
try:
instance = future.result()
except Exception:
logger.exception(
"Provisioning failed for %s replica %s",
job["agent_name"], job["replica_index"],
)
continue
provisioned.append(
(job["agent_name"], job["instance_id"], instance)
)
for agent_name, instance_id, instance in existing + provisioned:
self._add_instance_to_agent(agent_name, instance_id)
self._track_runtime(agent_name, instance["runtime_id"])
instances.append(instance)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ventis/controller/instance_manager.py` around lines 74 - 91, Update the
concurrent provisioning flow around _provision_one and future.result() to
isolate failures per job: apply the existing provisioning timeout policy, catch
and record/log each future’s exception without aborting ensure_instances, and
continue aggregating successful instances so _add_instance_to_agent,
_track_runtime, and the later routing snapshot still run for completed jobs.

@iidsample
iidsample merged commit 7524bdd into main Jul 21, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants