wukaixingxp commited on
Commit
f58914c
·
verified ·
1 Parent(s): d62ce76

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile works for both in-repo and standalone builds.
9
+
10
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
11
+ FROM ${BASE_IMAGE} AS builder
12
+
13
+ WORKDIR /app
14
+
15
+ # Build argument to control whether we're building standalone or in-repo
16
+ ARG BUILD_MODE=in-repo
17
+
18
+ # Install system dependencies for Julia
19
+ RUN apt-get update && apt-get install -y --no-install-recommends \
20
+ git \
21
+ curl \
22
+ ca-certificates \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ # Install juliaup and Julia 1.10
26
+ RUN curl -fsSL https://install.julialang.org | sh -s -- --yes --default-channel 1.10
27
+
28
+ # Add Julia to PATH
29
+ ENV PATH="/root/.juliaup/bin:${PATH}"
30
+
31
+ # Configure juliaup to prevent runtime network calls
32
+ # Set the default channel explicitly and mark juliaup as initialized
33
+ ENV JULIAUP_CHANNEL="1.10"
34
+ RUN juliaup default 1.10
35
+
36
+ # Verify Julia installation
37
+ RUN julia --version
38
+
39
+ # Precompile commonly used Julia packages (Test is built-in)
40
+ RUN julia -e 'using Test; println("Julia Test module ready")'
41
+
42
+ # Copy environment code (always at root of build context)
43
+ COPY . /app/env
44
+
45
+ # Ensure uv is available
46
+ RUN if ! command -v uv >/dev/null 2>&1; then \
47
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
48
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
49
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
50
+ fi
51
+
52
+ WORKDIR /app/env
53
+
54
+ # Install dependencies using uv sync
55
+ RUN --mount=type=cache,target=/root/.cache/uv \
56
+ if [ -f uv.lock ]; then \
57
+ uv sync --frozen --no-install-project --no-editable; \
58
+ else \
59
+ uv sync --no-install-project --no-editable; \
60
+ fi
61
+
62
+ RUN --mount=type=cache,target=/root/.cache/uv \
63
+ if [ -f uv.lock ]; then \
64
+ uv sync --frozen --no-editable; \
65
+ else \
66
+ uv sync --no-editable; \
67
+ fi
68
+
69
+ # Final runtime stage
70
+ FROM ${BASE_IMAGE}
71
+
72
+ WORKDIR /app
73
+
74
+ # Install Julia runtime dependencies
75
+ RUN apt-get update && apt-get install -y --no-install-recommends \
76
+ curl \
77
+ ca-certificates \
78
+ && rm -rf /var/lib/apt/lists/*
79
+
80
+ # Copy Julia installation from builder
81
+ # juliaup stores the manager in ~/.juliaup and actual Julia versions in ~/.julia/juliaup
82
+ COPY --from=builder /root/.juliaup /root/.juliaup
83
+ COPY --from=builder /root/.julia /root/.julia
84
+
85
+ # Create a direct symlink to Julia binary, bypassing the juliaup wrapper
86
+ # This prevents juliaup from trying to check for updates at runtime
87
+ # The juliaup wrapper (at ~/.juliaup/bin/julia) triggers network calls;
88
+ # the actual Julia binary is in ~/.julia/juliaup/julia-*/bin/julia
89
+ RUN echo "Looking for Julia binary..." && \
90
+ ls -la /root/.julia/juliaup/ && \
91
+ JULIA_BIN=$(find /root/.julia/juliaup -type f -executable -name "julia" 2>/dev/null | head -1) && \
92
+ if [ -z "$JULIA_BIN" ]; then \
93
+ echo "ERROR: Could not find Julia binary. Contents of .julia/juliaup:" && \
94
+ find /root/.julia -name "julia*" -ls && \
95
+ exit 1; \
96
+ fi && \
97
+ ln -sf "$JULIA_BIN" /usr/local/bin/julia && \
98
+ echo "Linked Julia from: $JULIA_BIN"
99
+
100
+ # Verify Julia works without network
101
+ RUN julia --version
102
+
103
+ ENV PATH="/usr/local/bin:${PATH}"
104
+
105
+ # Disable juliaup's version update checks (as fallback if juliaup wrapper is used)
106
+ ENV JULIAUP_CHANNEL="1.10"
107
+
108
+ # Copy the virtual environment from builder
109
+ COPY --from=builder /app/env/.venv /app/.venv
110
+
111
+ # Copy the environment code
112
+ COPY --from=builder /app/env /app/env
113
+
114
+ # Set PATH to use the virtual environment
115
+ ENV PATH="/app/.venv/bin:$PATH"
116
+
117
+ # Set PYTHONPATH so imports work correctly
118
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
119
+
120
+ # Julia process pool configuration (can be overridden at runtime)
121
+ ENV JULIA_MAX_WORKERS=8
122
+ ENV JULIA_EXECUTION_TIMEOUT=120
123
+ ENV JULIA_LOG_FILE=/tmp/julia_env.log
124
+ ENV JULIA_LOG_LEVEL=INFO
125
+ ENV PYTHONUNBUFFERED=1
126
+
127
+ # Health check
128
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
129
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
130
+
131
+ # Expose port
132
+ EXPOSE 8000
133
+
134
+ # Run the FastAPI server
135
+ ENV ENABLE_WEB_INTERFACE=true
136
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,10 +1,234 @@
1
  ---
2
- title: Julia Env Test
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Julia Environment Server
3
+ emoji: 🔬
4
+ colorFrom: purple
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
+ - julia
13
  ---
14
 
15
+ # Julia Environment
16
+
17
+ A Julia code execution environment that runs Julia code with test result tracking and reward calculation. Perfect for reinforcement learning training with Julia programming tasks.
18
+
19
+ ## Quick Start
20
+
21
+ The simplest way to use the Julia environment is through the `JuliaEnv` class:
22
+
23
+ ```python
24
+ from envs.julia_env import JuliaAction, JuliaEnv
25
+
26
+ try:
27
+ # Create environment from Docker image
28
+ julia_env = JuliaEnv.from_docker_image("julia-env:latest")
29
+
30
+ # Reset
31
+ result = julia_env.reset()
32
+ print(f"Reset complete: exit_code={result.observation.exit_code}")
33
+
34
+ # Execute Julia code with tests
35
+ action = JuliaAction(
36
+ core_code="""
37
+ function multiply(a, b)
38
+ return a * b
39
+ end
40
+ """,
41
+ test_code="""
42
+ using Test
43
+ @test multiply(3, 4) == 12
44
+ @test multiply(5, 6) == 30
45
+ """
46
+ )
47
+
48
+ result = julia_env.step(action)
49
+ print(f"Tests passed: {result.observation.tests_passed}")
50
+ print(f"Tests failed: {result.observation.tests_failed}")
51
+ print(f"Code compiles: {result.observation.code_compiles}")
52
+ print(f"Reward: {result.reward}")
53
+
54
+ finally:
55
+ # Always clean up
56
+ julia_env.close()
57
+ ```
58
+
59
+ That's it! The `JuliaEnv.from_docker_image()` method handles:
60
+ - Starting the Docker container
61
+ - Waiting for the server to be ready
62
+ - Connecting to the environment
63
+ - Container cleanup when you call `close()`
64
+
65
+ ## Building the Docker Image
66
+
67
+ Before using the environment, you need to build the Docker image:
68
+
69
+ ```bash
70
+ # From the julia_env directory
71
+ cd envs/julia_env
72
+ docker build -t julia-env:latest -f server/Dockerfile .
73
+ ```
74
+
75
+ ## Environment Details
76
+
77
+ ### Action
78
+
79
+ **JuliaAction**: Contains two fields for Julia code execution
80
+ - `core_code` (str) - The main Julia code to execute (e.g., function definitions)
81
+ - `test_code` (str) - Test code using Julia's `Test` module (e.g., `@test` statements)
82
+
83
+ ### Observation
84
+
85
+ **JuliaObservation**: Contains the execution results and test outcomes
86
+ - `stdout` (str) - Standard output from Julia execution
87
+ - `stderr` (str) - Standard error from Julia execution
88
+ - `exit_code` (int) - Exit code (0 for success, non-zero for errors)
89
+ - `tests_passed` (int) - Number of tests that passed
90
+ - `tests_failed` (int) - Number of tests that failed
91
+ - `code_compiles` (bool) - Whether the core code compiled/executed successfully
92
+
93
+ ### State
94
+
95
+ **JuliaState**: Tracks episode execution state
96
+ - `episode_id` (str) - Unique identifier for the episode
97
+ - `step_count` (int) - Number of steps taken in the episode
98
+ - `last_exit_code` (int) - Exit code from the last execution
99
+ - `last_code_compiles` (bool) - Whether the last code compiled successfully
100
+ - `total_tests_passed` (int) - Cumulative number of tests passed in the episode
101
+ - `total_tests_failed` (int) - Cumulative number of tests failed in the episode
102
+
103
+ ### Reward Calculation
104
+
105
+ The environment calculates rewards based on execution success and test results:
106
+ - Code compiles successfully: Base reward
107
+ - Tests pass: Additional reward per test
108
+ - Tests fail or code doesn't compile: Negative reward
109
+
110
+ See `server/julia_transforms.py` for detailed reward logic.
111
+
112
+ ## Features
113
+
114
+ - ✅ Execute Julia code in isolated subprocess
115
+ - ✅ Parse Julia `Test` module output (tests passed/failed)
116
+ - ✅ Calculate rewards based on execution results and test outcomes
117
+ - ✅ Safety transforms for output truncation (prevents excessive output)
118
+ - ✅ Docker support for reproducible execution
119
+ - ✅ Compatible with GRPO and other RL training frameworks
120
+
121
+ ## Advanced Usage
122
+
123
+ ### Connecting to an Existing Server
124
+
125
+ If you already have a Julia environment server running, you can connect directly:
126
+
127
+ ```python
128
+ from envs.julia_env import JuliaEnv, JuliaAction
129
+
130
+ # Connect to existing server
131
+ julia_env = JuliaEnv(base_url="http://localhost:8000")
132
+
133
+ # Use as normal
134
+ result = julia_env.reset()
135
+ result = julia_env.step(JuliaAction(
136
+ core_code="println(2 + 2)",
137
+ test_code=""
138
+ ))
139
+ ```
140
+
141
+ Note: When connecting to an existing server, `julia_env.close()` will NOT stop the server.
142
+
143
+ ### Custom Timeout
144
+
145
+ The Julia environment uses a longer timeout (180s) by default to accommodate Julia compilation and execution:
146
+
147
+ ```python
148
+ # Custom timeout (in seconds)
149
+ julia_env = JuliaEnv(base_url="http://localhost:8000", message_timeout_s=300.0)
150
+ ```
151
+
152
+ ### Running with Docker Directly
153
+
154
+ ```bash
155
+ # Run with default settings (port 8000)
156
+ docker run -d -p 8000:8000 --name julia-env julia-env:latest
157
+
158
+ # Check health
159
+ curl http://localhost:8000/health
160
+
161
+ # View logs
162
+ docker logs -f julia-env
163
+ ```
164
+
165
+ ## Example Code
166
+
167
+ Here's a more complex example demonstrating test-driven development:
168
+
169
+ ```python
170
+ from envs.julia_env import JuliaAction, JuliaEnv
171
+
172
+ julia_env = JuliaEnv.from_docker_image("julia-env:latest")
173
+
174
+ try:
175
+ # Reset the environment
176
+ julia_env.reset()
177
+
178
+ # Step 1: Define a function with tests
179
+ action = JuliaAction(
180
+ core_code="""
181
+ function fibonacci(n)
182
+ if n <= 1
183
+ return n
184
+ end
185
+ return fibonacci(n-1) + fibonacci(n-2)
186
+ end
187
+ """,
188
+ test_code="""
189
+ using Test
190
+ @test fibonacci(0) == 0
191
+ @test fibonacci(1) == 1
192
+ @test fibonacci(5) == 5
193
+ @test fibonacci(10) == 55
194
+ """
195
+ )
196
+
197
+ result = julia_env.step(action)
198
+ print(f"Step 1 - Tests passed: {result.observation.tests_passed}/4")
199
+ print(f"Step 1 - Reward: {result.reward}")
200
+
201
+ # Get current state
202
+ state = julia_env.state()
203
+ print(f"Total tests passed so far: {state.total_tests_passed}")
204
+ print(f"Total tests failed so far: {state.total_tests_failed}")
205
+
206
+ finally:
207
+ julia_env.close()
208
+ ```
209
+
210
+ ## Compatibility
211
+
212
+ This environment is compatible with:
213
+ - **torchforge**: For GRPO training with Julia tasks
214
+ - **TRL**: Hugging Face's RL library
215
+ - **Any OpenEnv-compatible RL framework**
216
+
217
+ ## Development
218
+
219
+ For local development without Docker:
220
+
221
+ ```bash
222
+ # Install Julia 1.10+ from https://julialang.org/downloads/
223
+
224
+ # Install Python dependencies
225
+ cd envs/julia_env
226
+ pip install -e .
227
+
228
+ # Run the server locally
229
+ uvicorn julia_env.server.app:app --host 0.0.0.0 --port 8000
230
+ ```
231
+
232
+ ## License
233
+
234
+ This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree.
__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Julia Environment - Code execution environment for RL training."""
8
+
9
+ from .client import JuliaEnv
10
+ from .models import JuliaAction, JuliaObservation, JuliaState
11
+
12
+ __all__ = ["JuliaAction", "JuliaObservation", "JuliaState", "JuliaEnv"]
13
+
client.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ JuliaEnv
9
+ --------
10
+ Client-side wrapper for the Julia environment server.
11
+
12
+ This client maintains a persistent WebSocket connection to the environment
13
+ server, enabling efficient multi-step interactions with lower latency.
14
+
15
+ - Users instantiate JuliaEnv with a base_url provided by the higher-level
16
+ vector/orchestration layer.
17
+ - Environment authors ship the Docker image that serves the API.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from openenv.core.client_types import StepResult
23
+ from openenv.core.env_client import EnvClient
24
+
25
+ from .models import JuliaAction, JuliaObservation, JuliaState
26
+
27
+
28
+ class JuliaEnv(EnvClient[JuliaAction, JuliaObservation, JuliaState]):
29
+ """
30
+ WebSocket client for the Julia Environment.
31
+
32
+ This client connects to a JuliaEnvironment server and provides
33
+ methods to interact with it: reset(), step(), and state access.
34
+
35
+ The default message timeout is set to 180 seconds to accommodate:
36
+ - Server execution timeout: 120s
37
+ - Process pool worker wait: 30s
38
+ - Network overhead: 30s buffer
39
+
40
+ Example:
41
+ >>> # Connect to a running server
42
+ >>> client = JuliaEnv(base_url="http://localhost:8000")
43
+ >>> result = client.reset()
44
+ >>> print(result.observation.stdout)
45
+ >>>
46
+ >>> # Execute Julia code
47
+ >>> action = JuliaAction(
48
+ ... core_code='''
49
+ ... function multiply(a, b)
50
+ ... return a * b
51
+ ... end
52
+ ... ''',
53
+ ... test_code='''
54
+ ... using Test
55
+ ... @test multiply(3, 4) == 12
56
+ ... '''
57
+ ... )
58
+ >>> result = client.step(action)
59
+ >>> print(result.observation.tests_passed) # 1
60
+ >>> print(result.reward)
61
+
62
+ Example with Docker:
63
+ >>> # Automatically start container and connect
64
+ >>> client = JuliaEnv.from_docker_image("julia-env:latest")
65
+ >>> result = client.reset()
66
+ >>> result = client.step(JuliaAction(core_code="println(2 + 2)", test_code=""))
67
+ >>> print(result.observation.stdout) # "4\\n"
68
+ >>> client.close()
69
+ """
70
+
71
+ # Override default timeout to accommodate Julia execution + worker wait
72
+ DEFAULT_MESSAGE_TIMEOUT = 180.0 # 120s execution + 30s worker wait + 30s buffer
73
+
74
+ def __init__(
75
+ self,
76
+ base_url: str,
77
+ connect_timeout_s: float = 10.0,
78
+ message_timeout_s: float | None = None,
79
+ **kwargs,
80
+ ):
81
+ """
82
+ Initialize JuliaEnv client with appropriate timeout.
83
+
84
+ Args:
85
+ base_url: Base URL of the Julia environment server
86
+ connect_timeout_s: Timeout for establishing WebSocket connection
87
+ message_timeout_s: Timeout for receiving responses (default: 180.0)
88
+ **kwargs: Additional arguments passed to EnvClient
89
+ """
90
+ if message_timeout_s is None:
91
+ message_timeout_s = self.DEFAULT_MESSAGE_TIMEOUT
92
+ super().__init__(
93
+ base_url,
94
+ connect_timeout_s=connect_timeout_s,
95
+ message_timeout_s=message_timeout_s,
96
+ **kwargs,
97
+ )
98
+
99
+ # --- EnvClient abstract hooks ---
100
+
101
+ def _step_payload(self, action: JuliaAction) -> dict:
102
+ """
103
+ Convert JuliaAction to JSON payload for step request.
104
+
105
+ Args:
106
+ action: JuliaAction instance
107
+
108
+ Returns:
109
+ Dictionary representation suitable for JSON encoding
110
+ """
111
+ return {
112
+ "core_code": action.core_code,
113
+ "test_code": action.test_code,
114
+ }
115
+
116
+ def _parse_result(self, payload: dict) -> StepResult[JuliaObservation]:
117
+ """
118
+ Parse server response into StepResult[JuliaObservation].
119
+
120
+ Args:
121
+ payload: JSON response from server
122
+
123
+ Returns:
124
+ StepResult with JuliaObservation
125
+ """
126
+ obs_data = payload.get("observation", {})
127
+ observation = JuliaObservation(
128
+ stdout=obs_data.get("stdout", ""),
129
+ stderr=obs_data.get("stderr", ""),
130
+ exit_code=obs_data.get("exit_code", 0),
131
+ tests_passed=obs_data.get("tests_passed", 0),
132
+ tests_failed=obs_data.get("tests_failed", 0),
133
+ code_compiles=obs_data.get("code_compiles", True),
134
+ done=payload.get("done", False),
135
+ reward=payload.get("reward"),
136
+ )
137
+
138
+ return StepResult[JuliaObservation](
139
+ observation=observation,
140
+ reward=payload.get("reward"),
141
+ done=payload.get("done", False),
142
+ )
143
+
144
+ def _parse_state(self, payload: dict) -> JuliaState:
145
+ """
146
+ Parse server response into JuliaState object.
147
+
148
+ Args:
149
+ payload: JSON response from /state endpoint
150
+
151
+ Returns:
152
+ JuliaState object with episode metadata
153
+ """
154
+ return JuliaState(
155
+ episode_id=payload.get("episode_id"),
156
+ step_count=payload.get("step_count", 0),
157
+ last_exit_code=payload.get("last_exit_code", 0),
158
+ last_code_compiles=payload.get("last_code_compiles", True),
159
+ total_tests_passed=payload.get("total_tests_passed", 0),
160
+ total_tests_failed=payload.get("total_tests_failed", 0),
161
+ )
models.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Data models for the Julia Environment.
9
+
10
+ The Julia environment executes Julia code and provides feedback through
11
+ compilation and unit test results.
12
+ """
13
+
14
+ from openenv.core.env_server import Action, Observation, State
15
+
16
+
17
+ class JuliaAction(Action):
18
+ """
19
+ Action for the Julia environment - code to execute.
20
+
21
+ Attributes:
22
+ core_code: Core Julia code to execute
23
+ test_code: Optional test code to execute. If not provided, only core_code runs.
24
+ """
25
+
26
+ core_code: str
27
+ test_code: str | None = None
28
+
29
+
30
+ class JuliaObservation(Observation):
31
+ """
32
+ Observation from the Julia environment - execution results.
33
+
34
+ Attributes:
35
+ stdout: Standard output from Julia execution
36
+ stderr: Standard error from Julia execution
37
+ exit_code: Exit code (0 = success, non-zero = error)
38
+ tests_passed: Number of tests passed (if tests were run)
39
+ tests_failed: Number of tests failed (if tests were run)
40
+ code_compiles: Whether the core code compiled/executed successfully
41
+ """
42
+
43
+ stdout: str = ""
44
+ stderr: str = ""
45
+ exit_code: int = 0
46
+ tests_passed: int = 0
47
+ tests_failed: int = 0
48
+ code_compiles: bool = True
49
+
50
+
51
+ class JuliaState(State):
52
+ """
53
+ State for Julia environment.
54
+
55
+ Attributes:
56
+ episode_id: Unique episode identifier
57
+ step_count: Number of steps taken in episode
58
+ last_exit_code: Exit code from last execution
59
+ last_code_compiles: Whether the last code compiled successfully
60
+ total_tests_passed: Cumulative tests passed in episode
61
+ total_tests_failed: Cumulative tests failed in episode
62
+ """
63
+
64
+ last_exit_code: int = 0
65
+ last_code_compiles: bool = True
66
+ total_tests_passed: int = 0
67
+ total_tests_failed: int = 0
openenv.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ name: julia_env
2
+ version: "0.1.0"
3
+ description: "Julia code execution environment for OpenEnv with test tracking and reward calculation"
4
+ action: JuliaAction
5
+ observation: JuliaObservation
pyproject.toml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-julia_env"
13
+ version = "0.1.0"
14
+ description = "Julia Environment for OpenEnv - Julia code execution with test tracking and reward calculation"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@main",
18
+ "fastapi>=0.115.0",
19
+ "pydantic>=2.0.0",
20
+ "uvicorn[standard]>=0.24.0",
21
+ "requests>=2.31.0",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = [
26
+ "pytest>=8.0.0",
27
+ "pytest-cov>=4.0.0",
28
+ ]
29
+
30
+ [project.scripts]
31
+ server = "julia_env.server.app:main"
32
+
33
+
34
+ [tool.setuptools]
35
+ packages = ["julia_env", "julia_env.server"]
36
+ package-dir = { "julia_env" = ".", "julia_env.server" = "server" }
37
+
38
+ [tool.setuptools.package-data]
39
+ julia_env = ["**/*.yaml", "**/*.yml", "**/*.jl"]
40
+ "julia_env.server" = ["*.jl"]
server/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Julia Environment Server."""
8
+
9
+ # Support both in-repo and standalone imports
10
+ try:
11
+ from .julia_codeact_env import JuliaCodeActEnv
12
+ from .julia_transforms import create_safe_julia_transform
13
+ except ImportError:
14
+ from server.julia_codeact_env import JuliaCodeActEnv
15
+ from server.julia_transforms import create_safe_julia_transform
16
+
17
+ __all__ = ["JuliaCodeActEnv", "create_safe_julia_transform"]
server/app.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the Julia Environment with concurrent execution support.
9
+
10
+ This module creates an HTTP server that exposes the JuliaCodeActEnv
11
+ over HTTP and WebSocket endpoints with optimized async execution for handling
12
+ multiple concurrent requests efficiently.
13
+
14
+ Features:
15
+ - WebSocket support for persistent sessions (required by OpenEnv clients)
16
+ - Julia Process Pool for 50-100x speedup on repeated executions
17
+ - Automatic error recovery and retry logic
18
+ - Comprehensive logging to file and console
19
+
20
+ Environment Variables:
21
+ - JULIA_MAX_WORKERS: Number of concurrent Julia executions (default: 8)
22
+ - JULIA_EXECUTION_TIMEOUT: Timeout in seconds (default: 120)
23
+ - JULIA_LOG_FILE: Log file path (default: /tmp/julia_env.log)
24
+ - JULIA_LOG_LEVEL: Log level (default: INFO)
25
+ - ENABLE_WEB_INTERFACE: Enable web interface (default: false)
26
+
27
+ Usage:
28
+ # Development (with auto-reload):
29
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
30
+
31
+ # Production:
32
+ uvicorn server.app:app --host 0.0.0.0 --port 8000
33
+
34
+ # Or run directly:
35
+ python -m server.app
36
+ """
37
+
38
+ import atexit
39
+ import logging
40
+ import os
41
+ import sys
42
+ from logging.handlers import RotatingFileHandler
43
+
44
+ # Support both in-repo and standalone imports
45
+ try:
46
+ # In-repo imports (when running from OpenEnv repository)
47
+ from openenv.core.env_server.http_server import create_app
48
+ from ..models import JuliaAction, JuliaObservation
49
+ from .julia_codeact_env import JuliaCodeActEnv
50
+ from .julia_executor import JuliaExecutor
51
+ except ImportError:
52
+ # Standalone imports (when environment is standalone)
53
+ from openenv.core.env_server.http_server import create_app
54
+ from models import JuliaAction, JuliaObservation
55
+ from server.julia_codeact_env import JuliaCodeActEnv
56
+ from server.julia_executor import JuliaExecutor
57
+
58
+ # Configuration from environment variables
59
+ MAX_WORKERS = int(os.getenv("JULIA_MAX_WORKERS", "8"))
60
+ EXECUTION_TIMEOUT = int(os.getenv("JULIA_EXECUTION_TIMEOUT", "120"))
61
+ LOG_FILE = os.getenv("JULIA_LOG_FILE", "/tmp/julia_env.log")
62
+ LOG_LEVEL = os.getenv("JULIA_LOG_LEVEL", "INFO")
63
+
64
+
65
+ def setup_logging():
66
+ """Configure logging to both file and console with rotation."""
67
+ # Configure both julia_env and openenv hierarchies to share handlers
68
+ julia_logger = logging.getLogger("julia_env")
69
+ openenv_logger = logging.getLogger("openenv")
70
+
71
+ julia_logger.setLevel(getattr(logging, LOG_LEVEL))
72
+ openenv_logger.setLevel(getattr(logging, LOG_LEVEL))
73
+
74
+ # Prevent duplicate handlers
75
+ if julia_logger.handlers:
76
+ return julia_logger
77
+
78
+ # Create formatters
79
+ detailed_formatter = logging.Formatter(
80
+ "%(asctime)s - %(name)s - [%(process)d:%(thread)d] - %(levelname)s - %(message)s",
81
+ datefmt="%Y-%m-%d %H:%M:%S",
82
+ )
83
+
84
+ # File handler with rotation (10MB max, keep 5 backup files)
85
+ try:
86
+ os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
87
+ file_handler = RotatingFileHandler(
88
+ LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8"
89
+ )
90
+ file_handler.setLevel(logging.DEBUG)
91
+ file_handler.setFormatter(detailed_formatter)
92
+ julia_logger.addHandler(file_handler)
93
+ openenv_logger.addHandler(file_handler)
94
+ except Exception as e:
95
+ print(f"Warning: Could not create log file {LOG_FILE}: {e}")
96
+
97
+ # Console handler
98
+ console_handler = logging.StreamHandler(sys.stdout)
99
+ console_handler.setLevel(logging.INFO)
100
+ console_handler.setFormatter(detailed_formatter)
101
+ julia_logger.addHandler(console_handler)
102
+ openenv_logger.addHandler(console_handler)
103
+
104
+ return julia_logger
105
+
106
+
107
+ # Setup logging
108
+ logger = setup_logging()
109
+
110
+
111
+ def initialize_julia_pool():
112
+ """Initialize the Julia process pool for better performance."""
113
+ port = int(os.getenv("PORT", "8000"))
114
+
115
+ logger.info("=" * 80)
116
+ logger.info("Starting Julia Environment Server")
117
+ logger.info(f"Container Port: {port}")
118
+ logger.info(f"Max Workers: {MAX_WORKERS}")
119
+ logger.info(f"Execution Timeout: {EXECUTION_TIMEOUT}s")
120
+ logger.info(f"Log File: {LOG_FILE}")
121
+ logger.info(f"Log Level: {LOG_LEVEL}")
122
+ logger.info("=" * 80)
123
+
124
+ # Enable Julia process pool for better performance
125
+ pool_enabled = JuliaExecutor.enable_process_pool(
126
+ size=MAX_WORKERS, timeout=EXECUTION_TIMEOUT
127
+ )
128
+ if pool_enabled:
129
+ logger.info(f"Julia process pool enabled with {MAX_WORKERS} workers")
130
+ else:
131
+ logger.warning("Julia process pool not available, using subprocess mode")
132
+
133
+ logger.info("Julia Environment Server initialized successfully")
134
+ print(f"Julia Environment Server started on port {port} with {MAX_WORKERS} concurrent workers")
135
+
136
+
137
+ def shutdown_julia_pool():
138
+ """Shutdown the Julia process pool."""
139
+ logger.info("Shutting down Julia Environment Server...")
140
+ JuliaExecutor.shutdown_pool()
141
+ logger.info("Julia process pool shutdown complete")
142
+ print("Julia Environment Server shutdown complete")
143
+
144
+
145
+ # Initialize the pool at module load time
146
+ initialize_julia_pool()
147
+
148
+ # Register cleanup on exit
149
+ atexit.register(shutdown_julia_pool)
150
+
151
+
152
+ # Create the app using OpenEnv's create_app for WebSocket support
153
+ # Pass the class (factory) instead of an instance for session support
154
+ app = create_app(
155
+ JuliaCodeActEnv,
156
+ JuliaAction,
157
+ JuliaObservation,
158
+ env_name="julia_env",
159
+ max_concurrent_envs=MAX_WORKERS,
160
+ )
161
+
162
+
163
+ # Add request logging middleware
164
+ import time as time_module
165
+ from starlette.middleware.base import BaseHTTPMiddleware
166
+ from starlette.requests import Request
167
+
168
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
169
+ """Log all incoming HTTP requests for debugging."""
170
+
171
+ async def dispatch(self, request: Request, call_next):
172
+ start_time = time_module.time()
173
+ path = request.url.path
174
+
175
+ # Skip health check logging to reduce noise
176
+ if path in ["/health", "/pool_status"]:
177
+ return await call_next(request)
178
+
179
+ logger.debug(f"HTTP Request: {request.method} {path}")
180
+
181
+ response = await call_next(request)
182
+
183
+ elapsed = time_module.time() - start_time
184
+ logger.debug(f"HTTP Response: {request.method} {path} -> {response.status_code} ({elapsed:.2f}s)")
185
+
186
+ return response
187
+
188
+ app.add_middleware(RequestLoggingMiddleware)
189
+
190
+
191
+ # Track active WebSocket connections
192
+ _active_ws_connections = 0
193
+ _total_ws_connections = 0
194
+
195
+
196
+ # Add custom health endpoint with pool metrics
197
+ @app.get("/pool_status")
198
+ async def pool_status():
199
+ """Get Julia process pool status."""
200
+ return {
201
+ "max_workers": MAX_WORKERS,
202
+ "timeout": EXECUTION_TIMEOUT,
203
+ "pool_enabled": JuliaExecutor.is_pool_enabled(),
204
+ "pool_metrics": JuliaExecutor.get_pool_metrics(),
205
+ "active_ws_connections": _active_ws_connections,
206
+ "total_ws_connections": _total_ws_connections,
207
+ }
208
+
209
+
210
+ def main():
211
+ """Main entry point for running the server."""
212
+ import uvicorn
213
+
214
+ port = int(os.getenv("PORT", "8000"))
215
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()
server/julia_codeact_env.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Julia Code Action Environment.
9
+
10
+ This module provides a server-side environment implementation for executing
11
+ Julia code actions using JuliaExecutor.
12
+ """
13
+
14
+ import logging
15
+ import re
16
+ import time
17
+ import uuid
18
+
19
+ # Support both in-repo and standalone imports
20
+ try:
21
+ # In-repo imports (when running from OpenEnv repository)
22
+ from openenv.core.env_server.interfaces import Action, Environment, Observation
23
+ from ..models import JuliaAction, JuliaObservation, JuliaState
24
+ from .julia_executor import JuliaExecutor
25
+ from .julia_transforms import create_safe_julia_transform
26
+ except ImportError:
27
+ # Standalone imports (when environment is standalone)
28
+ from openenv.core.env_server.interfaces import Action, Environment, Observation
29
+ from models import JuliaAction, JuliaObservation, JuliaState
30
+ from server.julia_executor import JuliaExecutor
31
+ from server.julia_transforms import create_safe_julia_transform
32
+
33
+ # Get logger for this module (inherits from julia_env logger)
34
+ logger = logging.getLogger("julia_env.codeact")
35
+
36
+ # Request counter for tracking
37
+ _request_counter = 0
38
+
39
+
40
+ def _detect_infinite_loop(code: str) -> tuple[bool, str]:
41
+ """
42
+ Detect potential infinite loops in Julia code.
43
+
44
+ This function scans for `while true` loops without break/return/error statements.
45
+
46
+ Args:
47
+ code: Julia code string to analyze
48
+
49
+ Returns:
50
+ Tuple of (has_infinite_loop: bool, reason: str)
51
+ """
52
+ # Remove comments and strings to avoid false positives
53
+ # Remove single-line comments
54
+ code_without_comments = re.sub(r'#.*', '', code)
55
+ # Remove multi-line strings (triple quotes)
56
+ code_without_comments = re.sub(r'""".*?"""', '', code_without_comments, flags=re.DOTALL)
57
+ # Remove single-line strings
58
+ code_without_comments = re.sub(r'"[^"]*"', '', code_without_comments)
59
+
60
+ # Find all while true blocks
61
+ while_true_pattern = r'\bwhile\s+true\b'
62
+ while_true_matches = list(re.finditer(while_true_pattern, code_without_comments, re.IGNORECASE))
63
+
64
+ if not while_true_matches:
65
+ return False, ""
66
+
67
+ # For each while true, check if there's a break/return/error in the same block
68
+ for match in while_true_matches:
69
+ start_pos = match.end()
70
+
71
+ # Find the end of this while block by counting 'while'/'end' pairs
72
+ # Simplified heuristic: look for break/return/error before the corresponding 'end'
73
+ remaining_code = code_without_comments[start_pos:]
74
+
75
+ # Extract potential loop body (up to next 'end' keyword)
76
+ # This is a simplified check - doesn't perfectly handle nested blocks
77
+ end_match = re.search(r'\bend\b', remaining_code)
78
+ if end_match:
79
+ loop_body = remaining_code[:end_match.start()]
80
+ else:
81
+ loop_body = remaining_code
82
+
83
+ # Check for loop exit mechanisms in this block
84
+ has_break = re.search(r'\bbreak\b', loop_body) is not None
85
+ has_return = re.search(r'\breturn\b', loop_body) is not None
86
+ has_error = re.search(r'\berror\(', loop_body) is not None
87
+ has_throw = re.search(r'\bthrow\(', loop_body) is not None
88
+ has_exit = re.search(r'\bexit\(', loop_body) is not None
89
+
90
+ if not (has_break or has_return or has_error or has_throw or has_exit):
91
+ loop_preview = loop_body[:100].strip()
92
+ return True, f"Infinite loop detected: 'while true' without break/return/error/throw. Preview: {loop_preview}"
93
+
94
+ return False, ""
95
+
96
+
97
+ class JuliaCodeActEnv(Environment):
98
+ """
99
+ Julia Code Action Environment for executing code and tracking state.
100
+
101
+ This environment executes Julia code submitted as JuliaAction during step,
102
+ maintains the last exit code in its state, and returns results wrapped
103
+ in JuliaObservation.
104
+
105
+ Example:
106
+ >>> env = JuliaCodeActEnv()
107
+ >>> obs = env.reset()
108
+ >>> action = JuliaAction(core_code='println("Hello, Julia!")', test_code='')
109
+ >>> obs = env.step(action)
110
+ >>> print(obs.stdout) # "Hello, Julia!\\n"
111
+ >>> print(obs.exit_code) # 0
112
+ >>> print(env.state.last_exit_code) # 0
113
+ """
114
+
115
+ # Allow concurrent sessions - each session has its own isolated state
116
+ SUPPORTS_CONCURRENT_SESSIONS = True
117
+
118
+ def __init__(self, use_process_pool: bool = True):
119
+ """
120
+ Initialize the Julia Code Act Environment.
121
+
122
+ Args:
123
+ use_process_pool: Use persistent Julia process pool for better performance
124
+ and to avoid Juliaup lock contention (default: True)
125
+ """
126
+ self._executor = JuliaExecutor(use_process_pool=use_process_pool)
127
+ self._state = JuliaState()
128
+ self.transform = create_safe_julia_transform()
129
+
130
+ def reset(self, **kwargs) -> Observation:
131
+ """
132
+ Reset environment for a fresh Julia execution session.
133
+ Returns an empty JuliaObservation with exit_code=0.
134
+
135
+ Note: Executor is reused to leverage process pool.
136
+ """
137
+ self._state = JuliaState(episode_id=str(uuid.uuid4()), step_count=0)
138
+ self._state.last_exit_code = 0
139
+ self._state.last_code_compiles = True
140
+ # Don't recreate executor - reuse it to leverage process pool
141
+
142
+ observation = JuliaObservation(
143
+ stdout="",
144
+ stderr="",
145
+ exit_code=0,
146
+ reward=0.0,
147
+ metadata={"core_code": "", "test_code": ""},
148
+ tests_passed=0,
149
+ tests_failed=0,
150
+ code_compiles=True,
151
+ )
152
+
153
+ observation = self._apply_transform(observation)
154
+ return observation
155
+
156
+ def step(self, action: Action, **kwargs) -> Observation:
157
+ """
158
+ Execute Julia code and return the result as JuliaObservation.
159
+
160
+ Optimized single-pass execution:
161
+ - Runs core_code + test_code together
162
+ - Infers compilation status from combined execution
163
+ - 2x faster than double execution
164
+
165
+ Args:
166
+ action: JuliaAction with core_code and optional test_code
167
+ **kwargs: Optional parameters including:
168
+ - timeout: Execution timeout in seconds (default: 120)
169
+ """
170
+ global _request_counter
171
+ _request_counter += 1
172
+ request_id = _request_counter
173
+
174
+ if not isinstance(action, JuliaAction):
175
+ logger.error(f"[REQ-{request_id}] Invalid action type: {type(action)}")
176
+ raise ValueError(f"Expected JuliaAction, got {type(action)}")
177
+
178
+ # Get timeout from kwargs (default handled by executor)
179
+ timeout = kwargs.get("timeout")
180
+
181
+ # Log request details
182
+ code_preview = action.core_code[:200] + "..." if len(action.core_code) > 200 else action.core_code
183
+ logger.info(f"[REQ-{request_id}] === NEW EXECUTION REQUEST ===")
184
+ logger.info(f"[REQ-{request_id}] Session: {self._state.episode_id}, Step: {self._state.step_count}")
185
+ logger.info(f"[REQ-{request_id}] Code length: {len(action.core_code)} chars, Test length: {len(action.test_code or '')} chars")
186
+ logger.debug(f"[REQ-{request_id}] Code preview: {code_preview}")
187
+ logger.info(f"[REQ-{request_id}] Timeout: {timeout}s" if timeout else f"[REQ-{request_id}] Timeout: default")
188
+
189
+ start_time = time.time()
190
+
191
+ # Single execution: Run core_code + test_code together (if test_code provided)
192
+ if action.test_code:
193
+ combined_code = action.core_code + "\n\n" + action.test_code
194
+ else:
195
+ combined_code = action.core_code
196
+
197
+ # Pre-execution check: detect infinite loops to avoid timeout
198
+ has_infinite_loop, loop_reason = _detect_infinite_loop(action.core_code)
199
+ if has_infinite_loop:
200
+ logger.warning(f"[REQ-{request_id}] INFINITE LOOP DETECTED: {loop_reason}")
201
+
202
+ # Update environment state
203
+ self._state.step_count += 1
204
+ self._state.last_exit_code = 1
205
+ self._state.last_code_compiles = True # Code compiles but has infinite loop
206
+ self._state.total_tests_passed = 0
207
+ self._state.total_tests_failed = 0
208
+
209
+ # Build observation with penalty
210
+ observation = JuliaObservation(
211
+ stdout="",
212
+ stderr=f"Infinite loop detected (pre-execution check): {loop_reason}",
213
+ exit_code=1,
214
+ reward=-1.0, # Penalize infinite loops
215
+ metadata={
216
+ "core_code": action.core_code,
217
+ "test_code": action.test_code or "",
218
+ "infinite_loop_detected": True,
219
+ "infinite_loop_reason": loop_reason,
220
+ },
221
+ tests_passed=0,
222
+ tests_failed=0,
223
+ code_compiles=True, # Code would compile, but not run
224
+ )
225
+
226
+ logger.info(
227
+ f"[REQ-{request_id}] RESULT: infinite_loop=True, "
228
+ f"tests_passed=0, tests_failed=0, reward=-1.00"
229
+ )
230
+
231
+ observation = self._apply_transform(observation)
232
+ return observation
233
+
234
+ try:
235
+ full_result = self._executor.run(combined_code, timeout=timeout)
236
+ execution_time = time.time() - start_time
237
+
238
+ logger.info(f"[REQ-{request_id}] Execution completed in {execution_time:.2f}s, exit_code={full_result.exit_code}")
239
+
240
+ # Log stderr if present (often contains errors or test output)
241
+ if full_result.stderr:
242
+ stderr_preview = full_result.stderr[:500] + "..." if len(full_result.stderr) > 500 else full_result.stderr
243
+ logger.debug(f"[REQ-{request_id}] Stderr: {stderr_preview}")
244
+
245
+ except Exception as e:
246
+ execution_time = time.time() - start_time
247
+ logger.error(f"[REQ-{request_id}] EXECUTION FAILED after {execution_time:.2f}s: {e}")
248
+ raise
249
+
250
+ # Parse test results from execution output
251
+ tests_passed, tests_failed = self._parse_test_results(
252
+ full_result.stdout, full_result.stderr
253
+ )
254
+
255
+ # Infer compilation status from execution
256
+ # If tests ran, code compiled successfully
257
+ # If exit_code != 0 and no tests ran, code didn't compile
258
+ code_compiles = (
259
+ full_result.exit_code == 0 # Clean execution
260
+ or tests_passed > 0 # Some tests passed (code must have compiled)
261
+ or tests_failed > 0 # Some tests failed (code compiled but tests failed)
262
+ )
263
+
264
+ # If no tests detected and non-zero exit, check for compilation errors
265
+ if not code_compiles and tests_passed == 0 and tests_failed == 0:
266
+ # Check stderr for compilation errors
267
+ stderr_lower = full_result.stderr.lower()
268
+ if any(
269
+ err in stderr_lower
270
+ for err in ["error", "syntax", "undefined", "loadError"]
271
+ ):
272
+ code_compiles = False
273
+ else:
274
+ # If no clear compilation error, assume it compiled
275
+ code_compiles = True
276
+
277
+ # Calculate reward based on compilation and test results
278
+ reward = self._calculate_reward(code_compiles, tests_passed, tests_failed)
279
+
280
+ # Log final results
281
+ logger.info(
282
+ f"[REQ-{request_id}] RESULT: compiles={code_compiles}, "
283
+ f"tests_passed={tests_passed}, tests_failed={tests_failed}, reward={reward:.2f}"
284
+ )
285
+
286
+ # Update environment state
287
+ self._state.step_count += 1
288
+ self._state.last_exit_code = full_result.exit_code
289
+ self._state.last_code_compiles = code_compiles
290
+ self._state.total_tests_passed = tests_passed
291
+ self._state.total_tests_failed = tests_failed
292
+
293
+ # Build observation
294
+ observation = JuliaObservation(
295
+ stdout=full_result.stdout,
296
+ stderr=full_result.stderr,
297
+ exit_code=full_result.exit_code,
298
+ reward=reward,
299
+ metadata={
300
+ "core_code": action.core_code,
301
+ "test_code": action.test_code or "",
302
+ },
303
+ tests_passed=tests_passed,
304
+ tests_failed=tests_failed,
305
+ code_compiles=code_compiles,
306
+ )
307
+
308
+ # Apply safety and quality transforms
309
+ observation = self._apply_transform(observation)
310
+
311
+ return observation
312
+
313
+ def _parse_test_results(self, stdout: str, stderr: str) -> tuple[int, int]:
314
+ """
315
+ Parse Julia test output to count passed/failed tests.
316
+
317
+ Julia's Test module outputs results like:
318
+ "Test Summary: | Pass Fail Total Time"
319
+ "Add function Tests | 1 1 2 1.5s"
320
+
321
+ Also checks error messages:
322
+ "Some tests did not pass: 1 passed, 1 failed, 0 errored, 0 broken."
323
+
324
+ Args:
325
+ stdout: Standard output from Julia execution
326
+ stderr: Standard error from Julia execution
327
+
328
+ Returns:
329
+ Tuple of (tests_passed, tests_failed)
330
+ """
331
+ # Combine stdout and stderr for analysis
332
+ passed = 0
333
+ failed = 0
334
+ output = stdout + "\n" + stderr
335
+
336
+ # Method 1: Look for "Some tests did not pass" error message
337
+ # Pattern: "Some tests did not pass: X passed, Y failed, Z errored, W broken."
338
+ error_pattern = r"Some tests did not pass:\s*(\d+)\s+passed,\s*(\d+)\s+failed,\s*(\d+)\s+errored"
339
+ match = re.search(error_pattern, output)
340
+
341
+ if match:
342
+ passed = int(match.group(1))
343
+ failed = int(match.group(2))
344
+ errored = int(match.group(3))
345
+ return passed, failed + errored # Treat errors as failures
346
+
347
+ # Method 2: Look for Test Summary table
348
+ # Multiple possible formats:
349
+ # All pass: "Test Summary: | Pass Total Time"
350
+ # "My Tests | 3 3 0.5s"
351
+ # Some fail: "Test Summary: | Pass Fail Total Time"
352
+ # "My Tests | 2 1 3 0.5s"
353
+ # All error: "Test Summary: | Error Total Time"
354
+ # "My Tests | 3 3 0.9s"
355
+ # Mixed: "Test Summary: | Pass Fail Error Total Time"
356
+ # "My Tests | 1 1 1 3 0.5s"
357
+ summary_lines = output.split("\n")
358
+ for i, line in enumerate(summary_lines):
359
+ if "Test Summary:" in line and i + 1 < len(summary_lines):
360
+ header_line = line
361
+ next_line = summary_lines[i + 1]
362
+
363
+ # Determine which columns are present
364
+ has_pass = "Pass" in header_line
365
+ has_fail = "Fail" in header_line
366
+ has_error = "Error" in header_line
367
+
368
+ # Extract all numbers from the line
369
+ all_numbers = re.findall(r"\d+", next_line)
370
+ if not all_numbers:
371
+ continue
372
+
373
+ # Last number is always Total, second to last is Time (skip it)
374
+ # Extract based on which columns exist
375
+ if has_pass and has_fail and has_error:
376
+ # Pass Fail Error Total Time
377
+ if len(all_numbers) >= 5:
378
+ passed = int(all_numbers[0])
379
+ failed = int(all_numbers[1]) + int(
380
+ all_numbers[2]
381
+ ) # Fail + Error
382
+ return passed, failed
383
+ elif has_pass and has_fail:
384
+ # Pass Fail Total Time
385
+ if len(all_numbers) >= 4:
386
+ passed = int(all_numbers[0])
387
+ failed = int(all_numbers[1])
388
+ return passed, failed
389
+ elif has_pass and has_error:
390
+ # Pass Error Total Time
391
+ if len(all_numbers) >= 4:
392
+ passed = int(all_numbers[0])
393
+ failed = int(all_numbers[1]) # Treat errors as failures
394
+ return passed, failed
395
+ elif has_fail and has_error:
396
+ # Fail Error Total Time (no passes)
397
+ if len(all_numbers) >= 4:
398
+ passed = 0
399
+ failed = int(all_numbers[0]) + int(all_numbers[1])
400
+ return passed, failed
401
+ elif has_pass:
402
+ # Pass Total Time (no failures/errors)
403
+ if len(all_numbers) >= 3:
404
+ passed = int(all_numbers[0])
405
+ failed = 0
406
+ return passed, failed
407
+ elif has_error:
408
+ # Error Total Time (all errors, no passes)
409
+ if len(all_numbers) >= 3:
410
+ passed = 0
411
+ failed = int(all_numbers[0]) # Treat all errors as failures
412
+ return passed, failed
413
+ elif has_fail:
414
+ # Fail Total Time (all failures, no passes)
415
+ if len(all_numbers) >= 3:
416
+ passed = 0
417
+ failed = int(all_numbers[0])
418
+ return passed, failed
419
+
420
+ return passed, failed
421
+
422
+ def _calculate_reward(
423
+ self, code_compiles: bool, tests_passed: int, tests_failed: int
424
+ ) -> float:
425
+ """
426
+ Normalized percentage-based reward for Julia GRPO.
427
+ Returns rewards in [-1, 1.5] range for comparability across problems.
428
+ """
429
+ if not code_compiles:
430
+ return -1.0
431
+
432
+ total_tests = tests_passed + tests_failed
433
+ if total_tests == 0:
434
+ return 0.0 # No signal when no tests run
435
+
436
+ pass_rate = tests_passed / total_tests
437
+
438
+ # Scaled 0-1 with bonus for perfection
439
+ if pass_rate == 1.0:
440
+ return 1.5 # Bonus for passing all tests
441
+ return pass_rate
442
+
443
+ def _apply_transform(self, observation: JuliaObservation) -> JuliaObservation:
444
+ """Apply safety and quality transforms to observation."""
445
+ if self.transform:
446
+ observation = self.transform(observation)
447
+ return observation
448
+
449
+ @property
450
+ def state(self) -> JuliaState:
451
+ """Return current environment state."""
452
+ return self._state
server/julia_executor.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Local Julia Executor with Process Pool Support.
9
+
10
+ This module provides a Julia code executor with:
11
+ - Proper process cleanup on timeout (no zombie processes)
12
+ - Robust error handling and logging
13
+ - Process group management for complete cleanup
14
+ - Automatic retry on transient failures
15
+ - Optional process pool for 50-100x speedup on repeated executions
16
+
17
+ Performance Modes:
18
+ - Standard mode: Spawn new process for each execution (default for single executions)
19
+ - Pool mode: Reuse persistent Julia processes (recommended for repeated executions)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import os
26
+ import shutil
27
+ import signal
28
+ import subprocess
29
+ import tempfile
30
+ import threading
31
+ import time
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+ from typing import Optional
35
+
36
+ # Use julia_env hierarchy to inherit handlers from app.py's setup_logging()
37
+ logger = logging.getLogger("julia_env.executor")
38
+
39
+
40
+ @dataclass
41
+ class CodeExecResult:
42
+ """Result of code execution."""
43
+
44
+ stdout: str
45
+ stderr: str
46
+ exit_code: int
47
+
48
+
49
+ # Try to import process pool (optional dependency)
50
+ try:
51
+ from .julia_process_pool import JuliaProcessPool
52
+
53
+ POOL_AVAILABLE = True
54
+ except ImportError:
55
+ POOL_AVAILABLE = False
56
+ JuliaProcessPool = None
57
+
58
+
59
+ class JuliaExecutor:
60
+ """
61
+ Executor for running Julia code with robust process management.
62
+
63
+ This class provides a safe interface to execute Julia code in isolation
64
+ and capture the results including stdout, stderr, and exit code.
65
+
66
+ Features:
67
+ - Proper timeout handling without zombie processes
68
+ - Process group cleanup for nested processes
69
+ - Automatic retry on transient failures
70
+ - Comprehensive logging for debugging
71
+ - Optional process pool for 50-100x speedup on repeated executions
72
+
73
+ Example:
74
+ >>> executor = JuliaExecutor()
75
+ >>> result = executor.run('println("Hello, Julia!")')
76
+ >>> print(result.stdout) # "Hello, Julia!\\n"
77
+ >>> print(result.exit_code) # 0
78
+ >>>
79
+ >>> # With process pool (recommended for repeated executions)
80
+ >>> JuliaExecutor.enable_process_pool(size=4)
81
+ >>> executor = JuliaExecutor(use_process_pool=True)
82
+ >>> for i in range(100):
83
+ ... result = executor.run(f'println({i})') # 50-100x faster!
84
+ >>> JuliaExecutor.shutdown_pool() # Clean up when done
85
+ """
86
+
87
+ # Class-level process pool (shared across all instances if enabled)
88
+ _shared_pool: Optional["JuliaProcessPool"] = None
89
+ _pool_lock = threading.Lock()
90
+ _pool_size: int = 0
91
+ _pool_timeout: int = 120
92
+
93
+ def __init__(
94
+ self,
95
+ timeout: Optional[int] = None,
96
+ max_retries: int = 0,
97
+ use_optimization_flags: bool = True,
98
+ use_process_pool: bool = True,
99
+ ):
100
+ """
101
+ Initialize the JuliaExecutor.
102
+
103
+ Args:
104
+ timeout: Maximum execution time in seconds. If None, reads from
105
+ JULIA_EXECUTION_TIMEOUT env var (default: 120 if not set)
106
+ max_retries: Number of retry attempts on transient failures (default: 0)
107
+ use_optimization_flags: Enable Julia performance flags (default: True)
108
+ use_process_pool: Use process pool if available (default: True)
109
+
110
+ Raises:
111
+ RuntimeError: If Julia executable is not found in PATH
112
+ """
113
+ # Read timeout from env var if not explicitly provided
114
+ if timeout is None:
115
+ timeout = int(os.getenv("JULIA_EXECUTION_TIMEOUT", "120"))
116
+ logger.debug(f"Executor timeout from JULIA_EXECUTION_TIMEOUT env var: {timeout}s")
117
+ self.timeout = timeout
118
+ self.max_retries = max_retries
119
+ self.use_optimization_flags = use_optimization_flags
120
+ self._use_process_pool = use_process_pool
121
+
122
+ # Find Julia executable in PATH
123
+ self.julia_path = shutil.which("julia")
124
+
125
+ if not self.julia_path:
126
+ # Try common installation paths
127
+ common_paths = [
128
+ os.path.expanduser("~/.juliaup/bin/julia"),
129
+ os.path.expanduser("~/.julia/bin/julia"),
130
+ "/usr/local/bin/julia",
131
+ "/usr/bin/julia",
132
+ ]
133
+
134
+ for path in common_paths:
135
+ if os.path.isfile(path) and os.access(path, os.X_OK):
136
+ self.julia_path = path
137
+ break
138
+
139
+ if not self.julia_path:
140
+ logger.warning(
141
+ "Julia executable not found in PATH or common locations. "
142
+ "Please install Julia: https://julialang.org/downloads/"
143
+ )
144
+
145
+ # Build optimized Julia command with performance flags
146
+ self.base_cmd = [self.julia_path] if self.julia_path else ["julia"]
147
+
148
+ if self.use_optimization_flags:
149
+ self.base_cmd.extend(
150
+ [
151
+ "--compile=min",
152
+ "--optimize=2",
153
+ "--startup-file=no",
154
+ "--history-file=no",
155
+ ]
156
+ )
157
+
158
+ logger.debug(f"JuliaExecutor initialized with Julia at: {self.julia_path}")
159
+ logger.debug(f"Timeout: {self.timeout}s, Max retries: {self.max_retries}")
160
+
161
+ def _kill_process_tree(
162
+ self, proc: subprocess.Popen, script_file: Optional[str] = None
163
+ ) -> None:
164
+ """
165
+ Terminate a process and all its children.
166
+
167
+ This prevents zombie processes by ensuring complete cleanup.
168
+
169
+ Args:
170
+ proc: The subprocess.Popen instance to terminate
171
+ script_file: Optional script file path (for logging)
172
+ """
173
+ if proc.poll() is None: # Process is still running
174
+ try:
175
+ # Try graceful termination first
176
+ logger.warning(f"Terminating process {proc.pid} gracefully...")
177
+ proc.terminate()
178
+
179
+ # Wait up to 2 seconds for graceful termination
180
+ try:
181
+ proc.wait(timeout=2.0)
182
+ logger.debug(f"Process {proc.pid} terminated gracefully")
183
+ return
184
+ except subprocess.TimeoutExpired:
185
+ logger.warning(
186
+ f"Process {proc.pid} did not terminate, forcing kill..."
187
+ )
188
+
189
+ # Force kill if still running
190
+ proc.kill()
191
+ try:
192
+ proc.wait(timeout=2.0)
193
+ logger.debug(f"Process {proc.pid} killed forcefully")
194
+ except subprocess.TimeoutExpired:
195
+ pass
196
+
197
+ except Exception as e:
198
+ logger.error(f"Error killing process {proc.pid}: {e}")
199
+
200
+ # Last resort: try killing via process group
201
+ try:
202
+ if hasattr(os, "killpg"):
203
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
204
+ logger.debug(f"Killed process group for {proc.pid}")
205
+ except Exception as pg_error:
206
+ logger.error(f"Failed to kill process group: {pg_error}")
207
+
208
+ def run(self, code: str, timeout: Optional[int] = None) -> CodeExecResult:
209
+ """
210
+ Execute Julia code and return the result with robust error handling.
211
+
212
+ This method provides:
213
+ - Automatic retry on transient failures
214
+ - Proper timeout handling without zombie processes
215
+ - Process group cleanup for nested processes
216
+ - Comprehensive error logging
217
+ - Optional process pool for 50-100x speedup
218
+
219
+ Args:
220
+ code: Julia code string to execute
221
+ timeout: Override default timeout (seconds). If None, uses pool's
222
+ configured timeout (when using pool) or instance timeout.
223
+
224
+ Returns:
225
+ CodeExecResult containing stdout, stderr, and exit_code
226
+ """
227
+ # Use process pool if enabled and available
228
+ # Pass timeout as-is (None means use pool's configured default)
229
+ if self._use_process_pool and JuliaExecutor._shared_pool is not None:
230
+ try:
231
+ return JuliaExecutor._shared_pool.execute(code, timeout=timeout)
232
+ except Exception as e:
233
+ logger.warning(
234
+ f"Process pool execution failed: {e}, falling back to subprocess"
235
+ )
236
+ # Fall through to standard execution
237
+
238
+ # For subprocess fallback, apply instance default if timeout not specified
239
+ if timeout is None:
240
+ timeout = self.timeout
241
+
242
+ # Check if Julia is available
243
+ if not self.julia_path:
244
+ return CodeExecResult(
245
+ stdout="",
246
+ stderr="Julia not found in PATH. Please install Julia.",
247
+ exit_code=127,
248
+ )
249
+
250
+ code_file = None
251
+
252
+ for attempt in range(self.max_retries + 1):
253
+ proc = None
254
+
255
+ try:
256
+ # Create temporary file for Julia code
257
+ with tempfile.NamedTemporaryFile(
258
+ mode="w", suffix=".jl", delete=False, encoding="utf-8"
259
+ ) as f:
260
+ f.write(code)
261
+ code_file = f.name
262
+
263
+ script_name = Path(code_file).name
264
+ logger.debug(
265
+ f"[Attempt {attempt + 1}/{self.max_retries + 1}] Executing: {script_name}"
266
+ )
267
+
268
+ # Start process with Popen for better control
269
+ start_time = time.time()
270
+
271
+ # On Unix systems, use process groups for better cleanup
272
+ kwargs = {
273
+ "stdout": subprocess.PIPE,
274
+ "stderr": subprocess.PIPE,
275
+ "text": True,
276
+ }
277
+
278
+ # Create new process group on Unix systems
279
+ if hasattr(os, "setpgrp"):
280
+ kwargs["preexec_fn"] = os.setpgrp
281
+
282
+ proc = subprocess.Popen(self.base_cmd + [code_file], **kwargs)
283
+
284
+ logger.debug(f"Started Julia process {proc.pid}")
285
+
286
+ # Wait for process with timeout
287
+ try:
288
+ stdout, stderr = proc.communicate(timeout=timeout)
289
+ exit_code = proc.returncode
290
+ elapsed = time.time() - start_time
291
+
292
+ logger.debug(
293
+ f"Julia execution completed in {elapsed:.2f}s (exit: {exit_code})"
294
+ )
295
+
296
+ # Clean up temp file
297
+ self._cleanup_temp_file(code_file)
298
+
299
+ return CodeExecResult(
300
+ stdout=stdout,
301
+ stderr=stderr,
302
+ exit_code=exit_code,
303
+ )
304
+
305
+ except subprocess.TimeoutExpired:
306
+ logger.error(
307
+ f"Julia execution timed out after {timeout}s "
308
+ f"(attempt {attempt + 1}/{self.max_retries + 1})"
309
+ )
310
+
311
+ # CRITICAL: Kill the process AND all its children
312
+ self._kill_process_tree(proc, code_file)
313
+
314
+ # If this was our last retry, return timeout error
315
+ if attempt >= self.max_retries:
316
+ self._cleanup_temp_file(code_file)
317
+ return CodeExecResult(
318
+ stdout="",
319
+ stderr=f"Execution timed out after {timeout}s",
320
+ exit_code=124, # Standard timeout exit code
321
+ )
322
+
323
+ # Wait before retry
324
+ time.sleep(1.0)
325
+ continue
326
+
327
+ except FileNotFoundError:
328
+ logger.error(f"Julia executable not found at {self.julia_path}")
329
+ return CodeExecResult(
330
+ stdout="",
331
+ stderr=f"Julia executable not found: {self.julia_path}",
332
+ exit_code=127,
333
+ )
334
+
335
+ except Exception as e:
336
+ logger.error(
337
+ f"Error executing Julia (attempt {attempt + 1}/{self.max_retries + 1}): {e}"
338
+ )
339
+
340
+ # Try to kill process if it exists
341
+ if proc is not None and proc.poll() is None:
342
+ self._kill_process_tree(proc, code_file)
343
+
344
+ # If this was our last retry, return error
345
+ if attempt >= self.max_retries:
346
+ self._cleanup_temp_file(code_file)
347
+ return CodeExecResult(
348
+ stdout="",
349
+ stderr=f"Error executing Julia code: {str(e)}",
350
+ exit_code=1,
351
+ )
352
+
353
+ # Wait before retry
354
+ time.sleep(1.0)
355
+ continue
356
+
357
+ finally:
358
+ # Always ensure temp file is cleaned up
359
+ self._cleanup_temp_file(code_file)
360
+
361
+ # Should never reach here
362
+ return CodeExecResult(
363
+ stdout="",
364
+ stderr="Unexpected error: all retries exhausted",
365
+ exit_code=1,
366
+ )
367
+
368
+ def _cleanup_temp_file(self, code_file: Optional[str]) -> None:
369
+ """Clean up temporary file safely."""
370
+ if code_file and Path(code_file).exists():
371
+ try:
372
+ Path(code_file).unlink()
373
+ except Exception as e:
374
+ logger.debug(f"Could not delete temp file {code_file}: {e}")
375
+
376
+ @staticmethod
377
+ def enable_process_pool(size: int = 4, timeout: Optional[int] = None) -> bool:
378
+ """
379
+ Enable the shared Julia process pool for all JuliaExecutor instances.
380
+
381
+ This provides 50-100x speedup for repeated code executions by reusing
382
+ persistent Julia processes instead of spawning new ones.
383
+
384
+ Args:
385
+ size: Number of worker processes to create (default: 4)
386
+ timeout: Default timeout for code execution in seconds.
387
+ If None, reads from JULIA_EXECUTION_TIMEOUT env var (default: 120)
388
+
389
+ Returns:
390
+ True if pool was created successfully, False otherwise
391
+ """
392
+ if not POOL_AVAILABLE:
393
+ logger.warning(
394
+ "Process pool not available (julia_process_pool module not found). "
395
+ "Falling back to subprocess execution."
396
+ )
397
+ return False
398
+
399
+ # Read timeout from env var if not explicitly provided
400
+ if timeout is None:
401
+ timeout = int(os.getenv("JULIA_EXECUTION_TIMEOUT", "120"))
402
+
403
+ with JuliaExecutor._pool_lock:
404
+ if JuliaExecutor._shared_pool is not None:
405
+ logger.debug("Process pool already enabled")
406
+ return True
407
+
408
+ try:
409
+ logger.info(f"Enabling Julia process pool with {size} workers")
410
+ JuliaExecutor._shared_pool = JuliaProcessPool(size=size, timeout=timeout)
411
+ JuliaExecutor._pool_size = size
412
+ JuliaExecutor._pool_timeout = timeout
413
+ logger.info("Julia process pool enabled successfully")
414
+ return True
415
+ except Exception as e:
416
+ logger.error(f"Failed to enable process pool: {e}")
417
+ return False
418
+
419
+ @staticmethod
420
+ def shutdown_pool() -> None:
421
+ """
422
+ Shutdown the shared Julia process pool.
423
+
424
+ This should be called when you're done with all Julia executions
425
+ to properly clean up worker processes.
426
+ """
427
+ with JuliaExecutor._pool_lock:
428
+ if JuliaExecutor._shared_pool is not None:
429
+ logger.info("Shutting down Julia process pool")
430
+ try:
431
+ JuliaExecutor._shared_pool.shutdown()
432
+ except Exception as e:
433
+ logger.error(f"Error shutting down pool: {e}")
434
+ finally:
435
+ JuliaExecutor._shared_pool = None
436
+
437
+ @staticmethod
438
+ def is_pool_enabled() -> bool:
439
+ """Check if the process pool is currently enabled."""
440
+ with JuliaExecutor._pool_lock:
441
+ return JuliaExecutor._shared_pool is not None
442
+
443
+ @staticmethod
444
+ def get_pool_metrics() -> dict:
445
+ """Get metrics about the process pool."""
446
+ if JuliaExecutor._shared_pool is None:
447
+ return {
448
+ "enabled": False,
449
+ "pool_size": 0,
450
+ "available_workers": 0,
451
+ }
452
+
453
+ return {
454
+ "enabled": True,
455
+ "pool_size": JuliaExecutor._pool_size,
456
+ "timeout": JuliaExecutor._pool_timeout,
457
+ "available_workers": JuliaExecutor._pool_size,
458
+ }
server/julia_process_pool.py ADDED
@@ -0,0 +1,601 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Julia Process Pool for high-performance code execution.
9
+
10
+ This module provides a pool of persistent Julia processes that can be reused
11
+ for multiple code executions, eliminating the overhead of spawning new processes.
12
+
13
+ Expected speedup: 50-100x for repeated executions compared to spawning new processes.
14
+
15
+ Features:
16
+ - Persistent Julia processes (no startup overhead)
17
+ - Thread-safe process allocation
18
+ - Automatic recovery from process failures
19
+ - Proper cleanup on shutdown
20
+ - Timeout handling per execution
21
+
22
+ Example:
23
+ >>> pool = JuliaProcessPool(size=4, timeout=30)
24
+ >>> result = pool.execute("println('Hello, Julia!')")
25
+ >>> print(result.stdout) # "Hello, Julia!\\n"
26
+ >>> pool.shutdown() # Clean up all processes
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import atexit
32
+ import logging
33
+ import os
34
+ import shutil
35
+ import subprocess
36
+ import threading
37
+ import time
38
+ from collections import deque
39
+ from dataclasses import dataclass
40
+ from pathlib import Path
41
+ from typing import Optional
42
+
43
+ # Use julia_env hierarchy to inherit handlers from app.py's setup_logging()
44
+ logger = logging.getLogger("julia_env.pool")
45
+
46
+
47
+ @dataclass
48
+ class CodeExecResult:
49
+ """Result of code execution."""
50
+
51
+ stdout: str
52
+ stderr: str
53
+ exit_code: int
54
+
55
+
56
+ class JuliaWorkerProcess:
57
+ """
58
+ Single Julia worker process that can execute code repeatedly.
59
+
60
+ This class manages communication with a persistent Julia REPL process
61
+ using a delimiter-based protocol.
62
+ """
63
+
64
+ # Communication protocol delimiters
65
+ START_OUTPUT = "<<<START_OUTPUT>>>"
66
+ START_ERROR = "<<<START_ERROR>>>"
67
+ EXIT_CODE_PREFIX = "<<<EXIT_CODE:"
68
+ END_EXECUTION = "<<<END_EXECUTION>>>"
69
+ END_CODE = "<<<END_CODE>>>"
70
+
71
+ def __init__(
72
+ self,
73
+ worker_id: int,
74
+ julia_path: str,
75
+ worker_script: str,
76
+ optimization_flags: bool = True,
77
+ ):
78
+ """
79
+ Initialize a Julia worker process.
80
+
81
+ Args:
82
+ worker_id: Unique identifier for this worker
83
+ julia_path: Path to Julia executable
84
+ worker_script: Path to julia_repl_worker.jl script
85
+ optimization_flags: Enable Julia optimization flags
86
+ """
87
+ self.worker_id = worker_id
88
+ self.julia_path = julia_path
89
+ self.worker_script = worker_script
90
+ self.optimization_flags = optimization_flags
91
+ self.process: Optional[subprocess.Popen] = None
92
+ self.is_busy = False
93
+ self.is_healthy = True
94
+ self.lock = threading.Lock()
95
+
96
+ # Start the worker process
97
+ self._start_process()
98
+
99
+ def _start_process(self) -> None:
100
+ """Start the Julia worker process."""
101
+ cmd = [self.julia_path]
102
+
103
+ if self.optimization_flags:
104
+ cmd.extend(
105
+ [
106
+ "--compile=min",
107
+ "--optimize=2",
108
+ "--startup-file=no",
109
+ "--history-file=no",
110
+ ]
111
+ )
112
+
113
+ cmd.append(self.worker_script)
114
+
115
+ try:
116
+ self.process = subprocess.Popen(
117
+ cmd,
118
+ stdin=subprocess.PIPE,
119
+ stdout=subprocess.PIPE,
120
+ stderr=subprocess.PIPE,
121
+ text=True,
122
+ bufsize=1, # Line buffered
123
+ )
124
+
125
+ # Wait for "Julia worker ready" message on stderr
126
+ ready_msg = self.process.stderr.readline()
127
+ if "ready" not in ready_msg.lower():
128
+ raise RuntimeError(
129
+ f"Worker {self.worker_id} did not start properly: {ready_msg}"
130
+ )
131
+
132
+ self.is_healthy = True
133
+ logger.info(f"Worker {self.worker_id} started (PID: {self.process.pid})")
134
+
135
+ except Exception as e:
136
+ self.is_healthy = False
137
+ logger.error(f"Failed to start worker {self.worker_id}: {e}")
138
+ raise
139
+
140
+ def execute(self, code: str, timeout: Optional[int] = None) -> CodeExecResult:
141
+ """
142
+ Execute Julia code in this worker process.
143
+
144
+ Args:
145
+ code: Julia code to execute
146
+ timeout: Maximum execution time in seconds.
147
+ If None, reads from JULIA_EXECUTION_TIMEOUT env var (default: 120)
148
+
149
+ Returns:
150
+ CodeExecResult with stdout, stderr, and exit_code
151
+ """
152
+ # Read timeout from env var if not explicitly provided
153
+ if timeout is None:
154
+ timeout = int(os.getenv("JULIA_EXECUTION_TIMEOUT", "120"))
155
+
156
+ with self.lock:
157
+ if not self.is_healthy or self.process is None:
158
+ raise RuntimeError(f"Worker {self.worker_id} is not healthy")
159
+
160
+ self.is_busy = True
161
+
162
+ try:
163
+ # Send code to worker
164
+ self.process.stdin.write(code + "\n")
165
+ self.process.stdin.write(self.END_CODE + "\n")
166
+ self.process.stdin.flush()
167
+
168
+ # Use threading for proper timeout handling
169
+ # The blocking readline() would otherwise prevent timeout detection
170
+ result_container: dict = {
171
+ "stdout_lines": [],
172
+ "stderr_lines": [],
173
+ "exit_code": -1,
174
+ "completed": False,
175
+ "error": None,
176
+ }
177
+
178
+ def read_output():
179
+ """Read output in a separate thread."""
180
+ stdout_lines = []
181
+ stderr_lines = []
182
+ exit_code = -1
183
+ current_section = None
184
+
185
+ try:
186
+ while True:
187
+ line = self.process.stdout.readline()
188
+
189
+ if not line:
190
+ # EOF - process died
191
+ result_container["error"] = "Worker process died unexpectedly"
192
+ return
193
+
194
+ line = line.rstrip("\n")
195
+
196
+ # Check for delimiters
197
+ if line == self.START_OUTPUT:
198
+ current_section = "stdout"
199
+ continue
200
+ elif line == self.START_ERROR:
201
+ current_section = "stderr"
202
+ continue
203
+ elif line.startswith(self.EXIT_CODE_PREFIX):
204
+ # Parse exit code
205
+ exit_code_str = line[
206
+ len(self.EXIT_CODE_PREFIX) : -3
207
+ ] # Remove prefix and ">>>"
208
+ exit_code = int(exit_code_str)
209
+ continue
210
+ elif line == self.END_EXECUTION:
211
+ # Execution complete
212
+ break
213
+
214
+ # Accumulate output
215
+ if current_section == "stdout":
216
+ stdout_lines.append(line)
217
+ elif current_section == "stderr":
218
+ stderr_lines.append(line)
219
+
220
+ result_container["stdout_lines"] = stdout_lines
221
+ result_container["stderr_lines"] = stderr_lines
222
+ result_container["exit_code"] = exit_code
223
+ result_container["completed"] = True
224
+
225
+ except Exception as e:
226
+ result_container["error"] = f"Error reading from worker: {str(e)}"
227
+
228
+ # Start reader thread
229
+ reader_thread = threading.Thread(target=read_output, daemon=True)
230
+ reader_thread.start()
231
+
232
+ # Wait for completion with timeout
233
+ reader_thread.join(timeout=timeout)
234
+
235
+ if reader_thread.is_alive():
236
+ # Timeout - kill the process
237
+ logger.error(f"Worker {self.worker_id} execution timed out after {timeout}s")
238
+ self.is_healthy = False
239
+ self._kill_process()
240
+ return CodeExecResult(
241
+ stdout="",
242
+ stderr=f"Execution timed out after {timeout} seconds",
243
+ exit_code=-1,
244
+ )
245
+
246
+ # Check for errors
247
+ if result_container["error"]:
248
+ logger.error(f"Worker {self.worker_id}: {result_container['error']}")
249
+ self.is_healthy = False
250
+ return CodeExecResult(
251
+ stdout="".join(result_container["stdout_lines"]),
252
+ stderr=result_container["error"],
253
+ exit_code=-1,
254
+ )
255
+
256
+ # Reconstruct output (add newlines back)
257
+ stdout_lines = result_container["stdout_lines"]
258
+ stderr_lines = result_container["stderr_lines"]
259
+ stdout_str = "\n".join(stdout_lines) + ("\n" if stdout_lines else "")
260
+ stderr_str = "\n".join(stderr_lines) + ("\n" if stderr_lines else "")
261
+
262
+ return CodeExecResult(
263
+ stdout=stdout_str,
264
+ stderr=stderr_str,
265
+ exit_code=result_container["exit_code"],
266
+ )
267
+
268
+ finally:
269
+ self.is_busy = False
270
+
271
+ def _kill_process(self) -> None:
272
+ """Kill the worker process."""
273
+ if self.process is not None:
274
+ try:
275
+ self.process.terminate()
276
+ self.process.wait(timeout=2.0)
277
+ except Exception:
278
+ try:
279
+ self.process.kill()
280
+ self.process.wait(timeout=1.0)
281
+ except Exception:
282
+ pass
283
+
284
+ def shutdown(self) -> None:
285
+ """Shutdown the worker process gracefully."""
286
+ with self.lock:
287
+ if self.process is not None:
288
+ logger.info(f"Shutting down worker {self.worker_id}")
289
+ self._kill_process()
290
+ self.process = None
291
+ self.is_healthy = False
292
+
293
+
294
+ class JuliaProcessPool:
295
+ """
296
+ Pool of persistent Julia processes for high-performance code execution.
297
+
298
+ This class manages multiple Julia worker processes and distributes
299
+ code execution among them, providing significant speedup by eliminating
300
+ process startup overhead.
301
+
302
+ Thread-safe for concurrent access from multiple threads.
303
+
304
+ Example:
305
+ >>> pool = JuliaProcessPool(size=4)
306
+ >>>
307
+ >>> # Execute code
308
+ >>> result = pool.execute("println('Hello')")
309
+ >>>
310
+ >>> # Pool automatically manages workers
311
+ >>> results = [pool.execute(f"println({i})") for i in range(100)]
312
+ >>>
313
+ >>> # Cleanup when done
314
+ >>> pool.shutdown()
315
+ """
316
+
317
+ def __init__(
318
+ self,
319
+ size: int = 4,
320
+ timeout: Optional[int] = None,
321
+ julia_path: Optional[str] = None,
322
+ optimization_flags: bool = True,
323
+ auto_recover: bool = True,
324
+ ):
325
+ """
326
+ Initialize the Julia process pool.
327
+
328
+ Args:
329
+ size: Number of worker processes to create (default: 4)
330
+ timeout: Default timeout for code execution in seconds.
331
+ If None, reads from JULIA_EXECUTION_TIMEOUT env var (default: 120)
332
+ julia_path: Path to Julia executable (auto-detected if None)
333
+ optimization_flags: Enable Julia optimization flags (default: True)
334
+ auto_recover: Automatically restart failed workers (default: True)
335
+
336
+ Raises:
337
+ RuntimeError: If Julia executable is not found
338
+ """
339
+ self.size = size
340
+ # Read timeout from env var if not explicitly provided
341
+ if timeout is None:
342
+ timeout = int(os.getenv("JULIA_EXECUTION_TIMEOUT", "120"))
343
+ logger.info(f"Pool timeout from JULIA_EXECUTION_TIMEOUT env var: {timeout}s")
344
+ else:
345
+ logger.info(f"Pool timeout explicitly set: {timeout}s")
346
+ self.timeout = timeout
347
+ self.optimization_flags = optimization_flags
348
+ self.auto_recover = auto_recover
349
+
350
+ # Find Julia executable
351
+ if julia_path is None:
352
+ julia_path = self._find_julia_executable()
353
+
354
+ self.julia_path = julia_path
355
+
356
+ # Find worker script
357
+ self.worker_script = self._find_worker_script()
358
+
359
+ # Initialize workers
360
+ self.workers: list[JuliaWorkerProcess] = []
361
+ self.available_workers: deque[JuliaWorkerProcess] = deque()
362
+ self.pool_lock = threading.Lock()
363
+ self.shutdown_flag = False
364
+
365
+ # Create worker processes
366
+ logger.info(f"Creating Julia process pool with {size} workers")
367
+ for i in range(size):
368
+ try:
369
+ worker = JuliaWorkerProcess(
370
+ worker_id=i,
371
+ julia_path=self.julia_path,
372
+ worker_script=self.worker_script,
373
+ optimization_flags=self.optimization_flags,
374
+ )
375
+ self.workers.append(worker)
376
+ self.available_workers.append(worker)
377
+ except Exception as e:
378
+ logger.error(f"Failed to create worker {i}: {e}")
379
+ # Clean up partially created pool
380
+ self.shutdown()
381
+ raise RuntimeError(f"Failed to create worker pool: {e}")
382
+
383
+ logger.info(f"Julia process pool initialized with {len(self.workers)} workers")
384
+
385
+ # Register cleanup on exit
386
+ atexit.register(self.shutdown)
387
+
388
+ def _find_julia_executable(self) -> str:
389
+ """Find Julia executable in PATH or common locations."""
390
+ # Try shutil.which first
391
+ julia_path = shutil.which("julia")
392
+ if julia_path:
393
+ return julia_path
394
+
395
+ # Try common locations
396
+ common_paths = [
397
+ os.path.expanduser("~/.juliaup/bin/julia"),
398
+ os.path.expanduser("~/.julia/bin/julia"),
399
+ "/usr/local/bin/julia",
400
+ "/usr/bin/julia",
401
+ ]
402
+
403
+ for path in common_paths:
404
+ if os.path.isfile(path) and os.access(path, os.X_OK):
405
+ return path
406
+
407
+ raise RuntimeError(
408
+ "Julia executable not found. Please install Julia: "
409
+ "https://julialang.org/downloads/"
410
+ )
411
+
412
+ def _find_worker_script(self) -> str:
413
+ """Find the julia_repl_worker.jl script."""
414
+ # Try relative to this file
415
+ this_dir = Path(__file__).parent
416
+ worker_script = this_dir / "julia_repl_worker.jl"
417
+
418
+ if worker_script.exists():
419
+ return str(worker_script)
420
+
421
+ raise RuntimeError(
422
+ f"Worker script not found at {worker_script}. "
423
+ "Please ensure julia_repl_worker.jl is in the same directory."
424
+ )
425
+
426
+ def _get_available_worker(
427
+ self, timeout: float = 30.0
428
+ ) -> Optional[JuliaWorkerProcess]:
429
+ """
430
+ Get an available worker from the pool.
431
+
432
+ Args:
433
+ timeout: Maximum time to wait for a worker (seconds)
434
+
435
+ Returns:
436
+ Available worker or None if timeout
437
+ """
438
+ start_time = time.time()
439
+
440
+ while time.time() - start_time < timeout:
441
+ with self.pool_lock:
442
+ # Try to get healthy worker
443
+ while self.available_workers:
444
+ worker = self.available_workers.popleft()
445
+
446
+ if worker.is_healthy:
447
+ return worker
448
+
449
+ # Worker is unhealthy, try to recover
450
+ if self.auto_recover and not self.shutdown_flag:
451
+ worker = self._recover_worker(worker)
452
+ if worker.is_healthy:
453
+ return worker
454
+ # Recovery failed, continue to next worker
455
+
456
+ # No workers available, wait a bit
457
+ time.sleep(0.1)
458
+
459
+ logger.error("Timeout waiting for available worker")
460
+ return None
461
+
462
+ def _recover_worker(self, worker: JuliaWorkerProcess) -> JuliaWorkerProcess:
463
+ """
464
+ Attempt to recover an unhealthy worker by restarting it.
465
+
466
+ Args:
467
+ worker: The unhealthy worker to recover
468
+
469
+ Returns:
470
+ The recovered worker (new instance) or the original if recovery fails
471
+ """
472
+ logger.warning(
473
+ f"Worker {worker.worker_id} is unhealthy, attempting recovery"
474
+ )
475
+ try:
476
+ worker.shutdown()
477
+ new_worker = JuliaWorkerProcess(
478
+ worker_id=worker.worker_id,
479
+ julia_path=self.julia_path,
480
+ worker_script=self.worker_script,
481
+ optimization_flags=self.optimization_flags,
482
+ )
483
+ # Update in workers list
484
+ self.workers[new_worker.worker_id] = new_worker
485
+ logger.info(f"Worker {new_worker.worker_id} recovered successfully")
486
+ return new_worker
487
+ except Exception as e:
488
+ logger.error(
489
+ f"Failed to recover worker {worker.worker_id}: {e}"
490
+ )
491
+ # Return original worker - it will be retried next time
492
+ return worker
493
+
494
+ def _return_worker(self, worker: JuliaWorkerProcess) -> None:
495
+ """
496
+ Return a worker to the available pool.
497
+
498
+ If the worker is unhealthy and auto_recover is enabled, attempts to
499
+ recover the worker before returning it to the pool. This ensures
500
+ workers are not leaked when they fail during execution.
501
+ """
502
+ with self.pool_lock:
503
+ if self.shutdown_flag:
504
+ return
505
+
506
+ # If worker is unhealthy, try to recover it immediately
507
+ if not worker.is_healthy and self.auto_recover:
508
+ worker = self._recover_worker(worker)
509
+
510
+ # Always return the worker to the pool (healthy or not)
511
+ # Unhealthy workers will be recovered when next acquired
512
+ self.available_workers.append(worker)
513
+
514
+ def execute(self, code: str, timeout: Optional[int] = None) -> CodeExecResult:
515
+ """
516
+ Execute Julia code using an available worker from the pool.
517
+
518
+ Args:
519
+ code: Julia code to execute
520
+ timeout: Execution timeout in seconds (uses pool default if None)
521
+
522
+ Returns:
523
+ CodeExecResult with stdout, stderr, and exit_code
524
+ """
525
+ if self.shutdown_flag:
526
+ return CodeExecResult(
527
+ stdout="",
528
+ stderr="Process pool has been shut down",
529
+ exit_code=-1,
530
+ )
531
+
532
+ if timeout is None:
533
+ timeout = self.timeout
534
+
535
+ # Get available worker
536
+ worker = self._get_available_worker()
537
+
538
+ if worker is None:
539
+ return CodeExecResult(
540
+ stdout="",
541
+ stderr="No available worker (timeout waiting for worker)",
542
+ exit_code=-1,
543
+ )
544
+
545
+ try:
546
+ # Execute code in worker
547
+ result = worker.execute(code, timeout=timeout)
548
+ return result
549
+
550
+ finally:
551
+ # Return worker to pool
552
+ self._return_worker(worker)
553
+
554
+ def shutdown(self) -> None:
555
+ """
556
+ Shutdown all worker processes gracefully.
557
+
558
+ This method is automatically called on exit via atexit.
559
+ """
560
+ if self.shutdown_flag:
561
+ return
562
+
563
+ logger.info("Shutting down Julia process pool")
564
+ self.shutdown_flag = True
565
+
566
+ with self.pool_lock:
567
+ for worker in self.workers:
568
+ try:
569
+ worker.shutdown()
570
+ except Exception as e:
571
+ logger.error(f"Error shutting down worker: {e}")
572
+
573
+ self.workers.clear()
574
+ self.available_workers.clear()
575
+
576
+ logger.info("Julia process pool shutdown complete")
577
+
578
+ def get_stats(self) -> dict:
579
+ """Get pool statistics."""
580
+ with self.pool_lock:
581
+ healthy_workers = sum(1 for w in self.workers if w.is_healthy)
582
+ available = len(self.available_workers)
583
+
584
+ return {
585
+ "total_workers": self.size,
586
+ "healthy_workers": healthy_workers,
587
+ "available_workers": available,
588
+ "shutdown": self.shutdown_flag,
589
+ }
590
+
591
+ def __enter__(self):
592
+ """Context manager entry."""
593
+ return self
594
+
595
+ def __exit__(self, exc_type, exc_val, exc_tb):
596
+ """Context manager exit."""
597
+ self.shutdown()
598
+
599
+ def __del__(self):
600
+ """Ensure cleanup on garbage collection."""
601
+ self.shutdown()
server/julia_repl_worker.jl ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env julia
2
+
3
+ """
4
+ Julia REPL Worker for Process Pool
5
+
6
+ This script runs as a persistent Julia process that accepts code via stdin,
7
+ executes it, and returns results via stdout with delimiters.
8
+
9
+ Protocol:
10
+ - Input: Code block followed by "<<<END_CODE>>>"
11
+ - Output: Results with status markers:
12
+ - "<<<START_OUTPUT>>>" - stdout begins
13
+ - "<<<START_ERROR>>>" - stderr begins
14
+ - "<<<EXIT_CODE:N>>>" - exit code (0 = success, 1 = error)
15
+ - "<<<END_EXECUTION>>>" - execution complete
16
+ """
17
+
18
+ # Preload commonly used modules to reduce JIT compilation overhead
19
+ # This is done once at worker startup, so subsequent code executions are faster
20
+ using Test
21
+
22
+ # Warm up the Test framework and common operations by running simple tests
23
+ # This pre-compiles the @test and @testset macros plus common Julia operations
24
+ let
25
+ # Warm up Test framework
26
+ @testset "warmup" begin
27
+ @test 1 + 1 == 2
28
+ @test "hello" == "hello"
29
+ @test [1, 2, 3] == [1, 2, 3]
30
+ end
31
+
32
+ # Warm up common string operations
33
+ occursin("a", "abc")
34
+ replace("hello", "l" => "x")
35
+ split("a,b,c", ",")
36
+ join(["a", "b"], ",")
37
+ lowercase("ABC")
38
+ uppercase("abc")
39
+ strip(" hello ")
40
+
41
+ # Warm up common array operations
42
+ arr = [3, 1, 2]
43
+ push!(arr, 4)
44
+ pop!(arr)
45
+ append!(arr, [5, 6])
46
+ sort(arr)
47
+ reverse(arr)
48
+ length(arr)
49
+ isempty(Int[])
50
+
51
+ # Warm up common math operations
52
+ div(10, 3)
53
+ mod(10, 3)
54
+ abs(-5)
55
+ sqrt(4.0)
56
+ floor(Int, 3.7)
57
+ ceil(Int, 3.2)
58
+
59
+ # Warm up Dict operations
60
+ d = Dict("a" => 1, "b" => 2)
61
+ get(d, "a", 0)
62
+ haskey(d, "a")
63
+ keys(d)
64
+ values(d)
65
+ end
66
+
67
+ # Delimiters for communication protocol
68
+ const START_OUTPUT = "<<<START_OUTPUT>>>"
69
+ const START_ERROR = "<<<START_ERROR>>>"
70
+ const EXIT_CODE_PREFIX = "<<<EXIT_CODE:"
71
+ const END_EXECUTION = "<<<END_EXECUTION>>>"
72
+ const END_CODE = "<<<END_CODE>>>"
73
+
74
+ """
75
+ Execute code and capture output using pipes
76
+ """
77
+ function execute_code(code::String)
78
+ # Initialize return values
79
+ stdout_str = ""
80
+ stderr_str = ""
81
+ exit_code = 0
82
+
83
+ # Create pipes for output capture
84
+ out_pipe = Pipe()
85
+ err_pipe = Pipe()
86
+
87
+ try
88
+ # Execute with output redirected to pipes
89
+ redirect_stdout(out_pipe) do
90
+ redirect_stderr(err_pipe) do
91
+ try
92
+ # Execute the code using include_string which properly handles
93
+ # multiple statements including 'using' statements
94
+ include_string(Main, code)
95
+ catch e
96
+ # Execution error - write to stderr
97
+ exit_code = 1
98
+ showerror(stderr, e, catch_backtrace())
99
+ println(stderr)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Close write ends to signal EOF to readers
105
+ Base.close(out_pipe.in)
106
+ Base.close(err_pipe.in)
107
+
108
+ # Read captured output
109
+ stdout_str = read(out_pipe.out, String)
110
+ stderr_str = read(err_pipe.out, String)
111
+
112
+ # Close read ends
113
+ Base.close(out_pipe.out)
114
+ Base.close(err_pipe.out)
115
+
116
+ catch e
117
+ # Worker error
118
+ exit_code = 1
119
+
120
+ # Try to close pipes
121
+ try
122
+ Base.close(out_pipe)
123
+ Base.close(err_pipe)
124
+ catch
125
+ end
126
+
127
+ stderr_str = "Worker error: " * sprint(showerror, e)
128
+ end
129
+
130
+ return (stdout_str, stderr_str, exit_code)
131
+ end
132
+
133
+ """
134
+ Main loop: read code, execute, return results
135
+ """
136
+ function main()
137
+ # Signal that worker is ready
138
+ println(stderr, "Julia worker ready")
139
+ flush(stderr)
140
+
141
+ while true
142
+ try
143
+ # Read code until END_CODE delimiter
144
+ code_lines = String[]
145
+
146
+ while true
147
+ if eof(stdin)
148
+ println(stderr, "Worker received EOF, shutting down")
149
+ return
150
+ end
151
+
152
+ line = readline(stdin)
153
+
154
+ # Check for end of code
155
+ if line == END_CODE
156
+ break
157
+ end
158
+
159
+ push!(code_lines, line)
160
+ end
161
+
162
+ # If no code received, continue
163
+ if isempty(code_lines)
164
+ # Send empty response
165
+ println(START_OUTPUT)
166
+ println(START_ERROR)
167
+ println(EXIT_CODE_PREFIX, 0, ">>>")
168
+ println(END_EXECUTION)
169
+ flush(stdout)
170
+ continue
171
+ end
172
+
173
+ code = join(code_lines, "\n")
174
+
175
+ # Execute code and capture output
176
+ (stdout_str, stderr_str, exit_code) = execute_code(code)
177
+
178
+ # Send results with delimiters
179
+ println(START_OUTPUT)
180
+ print(stdout_str)
181
+ flush(stdout)
182
+
183
+ println(START_ERROR)
184
+ print(stderr_str)
185
+ flush(stdout)
186
+
187
+ println(EXIT_CODE_PREFIX, exit_code, ">>>")
188
+ println(END_EXECUTION)
189
+ flush(stdout)
190
+
191
+ catch e
192
+ # Worker error - report and continue
193
+ println(stderr, "Worker error: ", e)
194
+ flush(stderr)
195
+
196
+ # Send error response
197
+ println(START_OUTPUT)
198
+ println(START_ERROR)
199
+ println("Worker internal error: ", e)
200
+ println(EXIT_CODE_PREFIX, 1, ">>>")
201
+ println(END_EXECUTION)
202
+ flush(stdout)
203
+ end
204
+ end
205
+ end
206
+
207
+ # Run main loop
208
+ main()
209
+
server/julia_transforms.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ envs/julia_env/julia_transforms.py
9
+ --------------------------------
10
+ Safety and quality transforms for Julia code.
11
+ """
12
+
13
+ import re
14
+
15
+ # Support both in-repo and standalone imports
16
+ try:
17
+ # In-repo imports
18
+ from openenv.core.env_server.interfaces import Transform
19
+ from ..models import JuliaObservation
20
+ except ImportError:
21
+ # Standalone imports
22
+ from openenv.core.env_server.interfaces import Transform
23
+ from models import JuliaObservation
24
+
25
+
26
+ # -------------------------
27
+ # Safety Transform
28
+ # -------------------------
29
+ class JuliaSafetyTransform(Transform):
30
+ """Detects dangerous Julia operations and penalizes them with a negative reward."""
31
+
32
+ def __init__(self, penalty: float = -3.0):
33
+ self.penalty = penalty
34
+ self.dangerous_patterns = [
35
+ r"run\(",
36
+ r"read\(",
37
+ r"write\(",
38
+ r"unsafe_",
39
+ r"ccall\(",
40
+ r"Base\.exit",
41
+ r"Base\.kill",
42
+ r"rm\(", # file deletion
43
+ r"download\(" # downloading
44
+ ]
45
+
46
+ def __call__(self, observation):
47
+ # Only act on JuliaObservation objects
48
+ if not isinstance(observation, JuliaObservation):
49
+ return observation
50
+
51
+ # Extract executed code from metadata (core_code + test_code)
52
+ if observation.metadata:
53
+ code = observation.metadata.get("core_code", "") + "\n" + observation.metadata.get("test_code", "")
54
+ else:
55
+ code = ""
56
+
57
+ for pattern in self.dangerous_patterns:
58
+ if re.search(pattern, code):
59
+ # Apply penalty and record violation
60
+ observation.reward = (observation.reward or 0.0) + self.penalty
61
+ observation.metadata = observation.metadata or {}
62
+ observation.metadata["safety_violation"] = pattern
63
+ return observation
64
+
65
+ # Safe code gets neutral reward
66
+ observation.reward = observation.reward or 0.0
67
+ return observation
68
+
69
+
70
+ # -------------------------
71
+ # Factory
72
+ # -------------------------
73
+ def create_safe_julia_transform():
74
+ """Creates safety transform for Julia code."""
75
+ return JuliaSafetyTransform()
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Core OpenEnv dependencies
2
+ fastapi>=0.115.0
3
+ pydantic>=2.0.0
4
+ uvicorn[standard]>=0.24.0
5
+ requests>=2.31.0
6
+ websockets>=12.0
test_julia_env.sh ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Julia Environment Test Script
3
+ # Tests infinite loop timeout handling and worker recovery
4
+ #
5
+ # Usage: ./test_julia_env.sh [HOST] [PORT]
6
+ # Default: localhost:8000
7
+
8
+ set -e
9
+
10
+ HOST="${1:-localhost}"
11
+ PORT="${2:-8000}"
12
+ BASE_URL="http://${HOST}:${PORT}"
13
+
14
+ GREEN='\033[0;32m'
15
+ RED='\033[0;31m'
16
+ YELLOW='\033[1;33m'
17
+ NC='\033[0m' # No Color
18
+
19
+ echo "=============================================="
20
+ echo "Julia Environment Test Suite"
21
+ echo "Testing: ${BASE_URL}"
22
+ echo "=============================================="
23
+ echo ""
24
+
25
+ # Function to print test results
26
+ pass() {
27
+ echo -e "${GREEN}✓ PASS${NC}: $1"
28
+ }
29
+
30
+ fail() {
31
+ echo -e "${RED}✗ FAIL${NC}: $1"
32
+ FAILED=1
33
+ }
34
+
35
+ warn() {
36
+ echo -e "${YELLOW}! WARN${NC}: $1"
37
+ }
38
+
39
+ FAILED=0
40
+
41
+ # Test 1: Health Check
42
+ echo "--- Test 1: Health Check ---"
43
+ HEALTH=$(curl -s --max-time 10 "${BASE_URL}/health" 2>/dev/null || echo "TIMEOUT")
44
+ if [[ "$HEALTH" == *"healthy"* ]]; then
45
+ pass "Server is healthy"
46
+ echo " Response: $HEALTH"
47
+ else
48
+ fail "Health check failed: $HEALTH"
49
+ fi
50
+ echo ""
51
+
52
+ # Test 2: Pool Status
53
+ echo "--- Test 2: Pool Status ---"
54
+ POOL=$(curl -s --max-time 10 "${BASE_URL}/pool_status" 2>/dev/null || echo "TIMEOUT")
55
+ if [[ "$POOL" == *"pool_enabled"* ]]; then
56
+ pass "Pool status retrieved"
57
+ echo " Response: $POOL"
58
+ else
59
+ fail "Pool status failed: $POOL"
60
+ fi
61
+ echo ""
62
+
63
+ # Test 3: Basic Execution
64
+ echo "--- Test 3: Basic Julia Execution ---"
65
+ BASIC_RESULT=$(curl -s --max-time 30 -X POST "${BASE_URL}/step" \
66
+ -H "Content-Type: application/json" \
67
+ -d '{"action": {"core_code": "println(\"Hello from Julia!\")", "test_code": ""}}' 2>/dev/null || echo "TIMEOUT")
68
+ if [[ "$BASIC_RESULT" == *"Hello from Julia"* ]]; then
69
+ pass "Basic execution works"
70
+ echo " Response: $BASIC_RESULT"
71
+ else
72
+ fail "Basic execution failed: $BASIC_RESULT"
73
+ fi
74
+ echo ""
75
+
76
+ # Test 4: Tests Passing
77
+ echo "--- Test 4: Test Execution (Passing) ---"
78
+ TEST_RESULT=$(curl -s --max-time 30 -X POST "${BASE_URL}/step" \
79
+ -H "Content-Type: application/json" \
80
+ -d '{
81
+ "action": {
82
+ "core_code": "function fib(n)\n n <= 1 ? n : fib(n-1) + fib(n-2)\nend",
83
+ "test_code": "using Test\n@testset \"Fibonacci\" begin\n @test fib(0) == 0\n @test fib(1) == 1\n @test fib(5) == 5\n @test fib(10) == 55\nend"
84
+ }
85
+ }' 2>/dev/null || echo "TIMEOUT")
86
+ if [[ "$TEST_RESULT" == *"tests_passed"* ]] && [[ "$TEST_RESULT" == *"4"* ]]; then
87
+ pass "Test execution works (4 tests passed)"
88
+ echo " Response: $TEST_RESULT"
89
+ else
90
+ fail "Test execution failed: $TEST_RESULT"
91
+ fi
92
+ echo ""
93
+
94
+ # Test 5: Syntax Error Detection
95
+ echo "--- Test 5: Syntax Error Detection ---"
96
+ SYNTAX_RESULT=$(curl -s --max-time 30 -X POST "${BASE_URL}/step" \
97
+ -H "Content-Type: application/json" \
98
+ -d '{"action": {"core_code": "function broken(\n # missing end", "test_code": ""}}' 2>/dev/null || echo "TIMEOUT")
99
+ if [[ "$SYNTAX_RESULT" == *"code_compiles"*"false"* ]] || [[ "$SYNTAX_RESULT" == *"error"* ]] || [[ "$SYNTAX_RESULT" == *"Error"* ]]; then
100
+ pass "Syntax error detected"
101
+ echo " Response: $SYNTAX_RESULT"
102
+ else
103
+ warn "Syntax error may not be properly detected: $SYNTAX_RESULT"
104
+ fi
105
+ echo ""
106
+
107
+ # Test 6: Infinite Loop Timeout (CRITICAL TEST)
108
+ echo "--- Test 6: Infinite Loop Timeout (CRITICAL) ---"
109
+ echo " This test checks if infinite loops are properly terminated."
110
+ echo " Expected: Request should timeout and return an error within pool timeout."
111
+ echo " Starting..."
112
+ START_TIME=$(date +%s)
113
+ TIMEOUT_RESULT=$(curl -s --max-time 300 -X POST "${BASE_URL}/step" \
114
+ -H "Content-Type: application/json" \
115
+ -d '{"action": {"core_code": "while true\n sleep(1)\nend", "test_code": ""}}' 2>/dev/null || echo "CURL_TIMEOUT")
116
+ END_TIME=$(date +%s)
117
+ ELAPSED=$((END_TIME - START_TIME))
118
+
119
+ if [[ "$TIMEOUT_RESULT" == "CURL_TIMEOUT" ]]; then
120
+ fail "Request timed out after ${ELAPSED}s (curl timeout). Server may be stuck."
121
+ elif [[ "$TIMEOUT_RESULT" == *"timed out"* ]] || [[ "$TIMEOUT_RESULT" == *"Execution timed out"* ]]; then
122
+ pass "Infinite loop properly timed out after ${ELAPSED}s"
123
+ echo " Response: $TIMEOUT_RESULT"
124
+ elif [[ "$TIMEOUT_RESULT" == *"exit_code"*"-1"* ]]; then
125
+ pass "Infinite loop terminated with exit_code -1 after ${ELAPSED}s"
126
+ echo " Response: $TIMEOUT_RESULT"
127
+ else
128
+ warn "Unexpected response after ${ELAPSED}s: $TIMEOUT_RESULT"
129
+ fi
130
+ echo ""
131
+
132
+ # Test 7: Worker Recovery After Timeout
133
+ echo "--- Test 7: Worker Recovery After Timeout ---"
134
+ echo " Testing if the server can handle requests after a timeout..."
135
+ RECOVERY_RESULT=$(curl -s --max-time 30 -X POST "${BASE_URL}/step" \
136
+ -H "Content-Type: application/json" \
137
+ -d '{"action": {"core_code": "println(\"Recovery test successful!\")", "test_code": ""}}' 2>/dev/null || echo "TIMEOUT")
138
+ if [[ "$RECOVERY_RESULT" == *"Recovery test successful"* ]]; then
139
+ pass "Worker recovery successful - server still operational"
140
+ echo " Response: $RECOVERY_RESULT"
141
+ else
142
+ fail "Worker recovery failed: $RECOVERY_RESULT"
143
+ fi
144
+ echo ""
145
+
146
+ # Test 8: Pool Status After Timeout
147
+ echo "--- Test 8: Pool Status After Timeout ---"
148
+ POOL_AFTER=$(curl -s --max-time 10 "${BASE_URL}/pool_status" 2>/dev/null || echo "TIMEOUT")
149
+ if [[ "$POOL_AFTER" == *"pool_enabled"*"true"* ]]; then
150
+ pass "Pool is still enabled after timeout"
151
+ echo " Response: $POOL_AFTER"
152
+ else
153
+ fail "Pool status check failed: $POOL_AFTER"
154
+ fi
155
+ echo ""
156
+
157
+ # Test 9: Multiple Consecutive Timeouts
158
+ echo "--- Test 9: Multiple Consecutive Timeouts (3x) ---"
159
+ echo " Testing if multiple timeouts exhaust the worker pool..."
160
+ for i in 1 2 3; do
161
+ echo " Timeout request $i/3..."
162
+ curl -s --max-time 300 -X POST "${BASE_URL}/step" \
163
+ -H "Content-Type: application/json" \
164
+ -d '{"action": {"core_code": "while true; sleep(1); end", "test_code": ""}}' >/dev/null 2>&1
165
+ done
166
+
167
+ echo " Testing normal operation after 3 timeouts..."
168
+ AFTER_MULTI=$(curl -s --max-time 30 -X POST "${BASE_URL}/step" \
169
+ -H "Content-Type: application/json" \
170
+ -d '{"action": {"core_code": "println(\"Still working after 3 timeouts!\")", "test_code": ""}}' 2>/dev/null || echo "TIMEOUT")
171
+ if [[ "$AFTER_MULTI" == *"Still working after 3 timeouts"* ]]; then
172
+ pass "Server handles multiple consecutive timeouts correctly"
173
+ else
174
+ fail "Server failed after multiple timeouts: $AFTER_MULTI"
175
+ fi
176
+ echo ""
177
+
178
+ # Test 10: Concurrent Request Handling
179
+ echo "--- Test 10: Concurrent Request Handling ---"
180
+ echo " Sending 4 requests in parallel..."
181
+ # Create temp files for results
182
+ TMPDIR=$(mktemp -d)
183
+ for i in 1 2 3 4; do
184
+ curl -s --max-time 60 -X POST "${BASE_URL}/step" \
185
+ -H "Content-Type: application/json" \
186
+ -d "{\"action\": {\"core_code\": \"println(\\\"Request $i\\\")\", \"test_code\": \"\"}}" > "${TMPDIR}/result_${i}.txt" 2>&1 &
187
+ done
188
+ wait
189
+
190
+ CONCURRENT_SUCCESS=0
191
+ for i in 1 2 3 4; do
192
+ if grep -q "Request $i" "${TMPDIR}/result_${i}.txt" 2>/dev/null; then
193
+ ((CONCURRENT_SUCCESS++))
194
+ fi
195
+ done
196
+ rm -rf "$TMPDIR"
197
+
198
+ if [[ $CONCURRENT_SUCCESS -eq 4 ]]; then
199
+ pass "All 4 concurrent requests succeeded"
200
+ else
201
+ fail "Only $CONCURRENT_SUCCESS/4 concurrent requests succeeded"
202
+ fi
203
+ echo ""
204
+
205
+ # Final Summary
206
+ echo "=============================================="
207
+ echo "Test Summary"
208
+ echo "=============================================="
209
+ if [[ $FAILED -eq 0 ]]; then
210
+ echo -e "${GREEN}All tests passed!${NC}"
211
+ exit 0
212
+ else
213
+ echo -e "${RED}Some tests failed. Please review the output above.${NC}"
214
+ exit 1
215
+ fi