Skip to content

Commit 80013b7

Browse files
committed
Merge branch 'add/sweep_status_bg_worker' of https://github.com/transformerlab/transformerlab-app into add/sweep_status_bg_worker
2 parents 2d6f3d8 + 580ad65 commit 80013b7

50 files changed

Lines changed: 23451 additions & 31939 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coderabbit.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
reviews:
2+
profile: 'chill' # less aggressive than default
3+
4+
high_level_summary: true
5+
review_start_comment: false # disable the "starting review" comment
6+
diff_only: true # Stop reviewing old code that is not part of the PR
7+
request_changes_workflow: false # don't auto-block PRs
8+
poem: false
9+
10+
instructions: >
11+
Do NOT suggest adding type annotations.
12+
Do NOT comment on missing return type hints.
13+
Focus on:
14+
- runtime correctness
15+
- security issues
16+
- performance issues
17+
- race conditions
18+
- architectural risks
19+
20+
auto_review:
21+
enabled: true
22+
drafts: false # don't review draft PRs
23+
24+
path_filters:
25+
- '!**/*.md'
26+
- '!docs/**'
27+
- '!**/*.json'
28+
- '!**/*.lock'
29+
- '!dist/**'
30+
- '!build/**'
31+
32+
tools:
33+
ruff:
34+
enabled: false # we already run this

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ npm-debug.log.*
3535
# https://docs.cursor.com/context/ignore-files#default-ignore-list
3636
.cursor/
3737
.cursorignore
38+
.claude/
3839

3940
.env
4041
**/.env

api/transformerlab/compute_providers/local.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Local compute provider: runs tasks in a uv venv synced with the base environment."""
22

3+
import contextlib
34
import os
5+
import signal
46
import subprocess
57
import sys
68
from pathlib import Path
@@ -85,6 +87,48 @@ def _get_uv_pip_install_flags() -> str:
8587
return ""
8688

8789

90+
def _terminate_process_tree(pid: int, sig: int = signal.SIGTERM) -> None:
91+
"""
92+
Best-effort termination of a process and all of its descendants.
93+
94+
Uses psutil when available to walk the full process tree and then force-kill
95+
any survivors; otherwise falls back to killing the process group (if possible)
96+
and then the single pid.
97+
"""
98+
try:
99+
import psutil # type: ignore[import-not-found]
100+
except Exception:
101+
psutil = None # type: ignore[assignment]
102+
103+
if psutil is not None:
104+
try:
105+
parent = psutil.Process(pid)
106+
except psutil.NoSuchProcess:
107+
parent = None
108+
109+
if parent is not None:
110+
procs = [parent] + parent.children(recursive=True)
111+
112+
# First try graceful termination
113+
for proc in procs:
114+
with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied, psutil.Error):
115+
proc.send_signal(sig)
116+
117+
# Give processes a short window to exit, then force kill survivors
118+
gone, alive = psutil.wait_procs(procs, timeout=3) # type: ignore[assignment]
119+
for proc in alive:
120+
with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied, psutil.Error):
121+
proc.kill()
122+
return
123+
124+
# Fallback path when psutil is unavailable or parent no longer exists
125+
try:
126+
pgid = os.getpgid(pid)
127+
os.killpg(pgid, sig)
128+
except Exception:
129+
os.kill(pid, sig)
130+
131+
88132
def _is_process_zombie(pid: int) -> bool:
89133
"""
90134
Return True if the process is a zombie/defunct process.
@@ -172,18 +216,19 @@ def launch_cluster(self, cluster_name: str, config: ClusterConfig) -> Dict[str,
172216
if not job_dir or not os.path.isdir(job_dir):
173217
raise ValueError("Local provider requires workspace_dir (job directory) in provider_config")
174218
job_dir = Path(job_dir)
175-
venv_path = job_dir / "venv"
176-
venv_path.mkdir(parents=True, exist_ok=True)
177-
178-
self._ensure_venv_and_sync(venv_path)
179-
180219
# Use a per-job workspace directory as HOME for local runs so tools that
181220
# rely on ~ and $HOME resolve inside the job workspace instead of the
182221
# user's real home directory. This makes it easier to clone and run
183222
# code in an isolated workspace for each job.
184223
workspace_home = job_dir / "workspace"
185224
workspace_home.mkdir(parents=True, exist_ok=True)
186225

226+
# Create the venv inside the per-job workspace HOME directory so that all
227+
# environment state (including Python packages) lives under HOME.
228+
venv_path = workspace_home / "venv"
229+
230+
self._ensure_venv_and_sync(venv_path)
231+
187232
venv_bin = venv_path / "bin"
188233
env = os.environ.copy()
189234
env.update(config.env_vars or {})
@@ -226,7 +271,7 @@ def launch_cluster(self, cluster_name: str, config: ClusterConfig) -> Dict[str,
226271
}
227272

228273
def stop_cluster(self, cluster_name: str) -> Dict[str, Any]:
229-
"""Stop the local process for this cluster (SIGTERM)."""
274+
"""Stop the local process tree for this cluster (SIGTERM)."""
230275
job_dir = self.extra_config.get("workspace_dir")
231276
if not job_dir:
232277
return {
@@ -243,8 +288,8 @@ def stop_cluster(self, cluster_name: str) -> Dict[str, Any]:
243288
}
244289
try:
245290
pid = int(pid_file.read_text().strip())
246-
os.kill(pid, 15)
247-
return {"cluster_name": cluster_name, "status": "stopped", "message": "Sent SIGTERM"}
291+
_terminate_process_tree(pid, signal.SIGTERM)
292+
return {"cluster_name": cluster_name, "status": "stopped", "message": "Sent SIGTERM to process tree"}
248293
except (ValueError, ProcessLookupError, OSError) as e:
249294
return {"cluster_name": cluster_name, "status": "stopped", "message": str(e)}
250295

api/transformerlab/routers/compute_provider.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2523,6 +2523,16 @@ async def stop_cluster(
25232523
user_id_str = str(user_and_team["user"].id)
25242524
provider_instance = await get_provider_instance(provider, user_id=user_id_str, team_id=team_id)
25252525

2526+
# Local provider needs workspace_dir (job dir) to stop the correct process tree.
2527+
# Derive job_id from the standard "-job-<job_id>" suffix in the cluster name.
2528+
if provider.type == ProviderType.LOCAL.value and hasattr(provider_instance, "extra_config"):
2529+
job_id_segment = None
2530+
if "-job-" in cluster_name:
2531+
job_id_segment = cluster_name.rsplit("-job-", 1)[-1] or None
2532+
if job_id_segment is not None:
2533+
job_dir = await asyncio.to_thread(get_local_provider_job_dir, job_id_segment, org_id=team_id)
2534+
provider_instance.extra_config["workspace_dir"] = job_dir
2535+
25262536
# Stop cluster
25272537
result = await asyncio.to_thread(provider_instance.stop_cluster, cluster_name)
25282538

api/transformerlab/routers/experiment/jobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ async def get_provider_job_logs(
364364
async def get_tunnel_info_for_job(
365365
experimentId: str,
366366
job_id: str,
367-
tail_lines: int = Query(400, ge=100, le=2000),
367+
tail_lines: int = Query(1000, ge=100, le=5000),
368368
user_and_team=Depends(get_user_and_team),
369369
session: AsyncSession = Depends(get_async_session),
370370
):

knip.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"$schema": "https://unpkg.com/knip@5/schema.json",
3+
"entry": [
4+
"src/renderer/index.tsx",
5+
"src/renderer/instrument.ts",
6+
"src/main/**/*.ts",
7+
"src/__tests__/**/*.{ts,tsx}",
8+
".erb/configs/**/*.ts",
9+
".erb/scripts/**/*.ts"
10+
],
11+
"project": ["src/**/*.{ts,tsx,js,jsx}"],
12+
"ignore": [
13+
"api/**",
14+
"cli/**",
15+
"lab-sdk/**",
16+
"docs/**",
17+
"scripts/**",
18+
"release/**",
19+
"build/**",
20+
"dist/**",
21+
"test/**",
22+
"test-results/**",
23+
"playwright-report/**",
24+
"assets/**",
25+
"src/node_modules/**",
26+
"**/*.d.ts",
27+
"src/renderer/preload.d.ts"
28+
],
29+
"ignoreDependencies": [
30+
"@playwright/test",
31+
"playwright",
32+
"electron",
33+
"electron-builder",
34+
"electron-devtools-installer",
35+
"electron-notarize"
36+
],
37+
"webpack": {
38+
"entry": [".erb/configs/webpack.config.*.ts"]
39+
},
40+
"jest": {
41+
"entry": ["src/__tests__/**/*.{ts,tsx}"]
42+
}
43+
}

0 commit comments

Comments
 (0)