Skip to content

Commit ad583dd

Browse files
committed
increased limits, added busybox container to speed up cold starts, removed some useless stuff from readme
1 parent d7bd0e1 commit ad583dd

File tree

6 files changed

+122
-70
lines changed

6 files changed

+122
-70
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<img src="./files_for_readme/logo.png" alt="Integr8sCode Logo" width="200" height="200">
33
<h1 align="center"><b>Integr8sCode</b></h1>
44
</p>
5-
65
<p align="center">
76
<a href="https://github.com/HardMax71/Integr8sCode/actions/workflows/ruff.yml">
87
<img src="https://img.shields.io/github/actions/workflow/status/HardMax71/Integr8sCode/ruff.yml?branch=main&label=ruff&logo=python&logoColor=white" alt="Ruff Status" />
@@ -31,7 +30,6 @@
3130
<img src="https://sonarcloud.io/api/project_badges/measure?project=HardMax71_Integr8sCode&metric=bugs" alt="Bugs">
3231
</a>
3332
</p>
34-
---
3533

3634
Welcome to **Integr8sCode**! This is a platform where you can run Python scripts online with ease. Just paste your
3735
script, and the platform run it in an isolated environment within its own Kubernetes pod, complete with resource limits

backend/.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES=30
55
MONGODB_URL=mongodb://mongo:27017/integr8scode
66
KUBERNETES_CONFIG_PATH=/app/kubeconfig.yaml
77
KUBERNETES_CA_CERTIFICATE_PATH=/app/certs/k8s-ca.pem
8-
K8S_POD_CPU_LIMIT=100m
8+
K8S_POD_CPU_LIMIT=1000m
99
K8S_POD_MEMORY_LIMIT=128Mi
10-
K8S_POD_CPU_REQUEST=100m
10+
K8S_POD_CPU_REQUEST=200m
1111
K8S_POD_MEMORY_REQUEST=128Mi
1212
K8S_POD_EXECUTION_TIMEOUT=5
1313
RATE_LIMITS=100/minute

backend/app/scripts/entrypoint.sh

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,76 @@
11
#!/bin/sh
2+
#
3+
# Strict, portable POSIX-sh entrypoint that runs an arbitrary
4+
# command, captures its output, exit-code and coarse resource
5+
# usage, then prints a single-line JSON blob to stdout.
26

3-
# This script is written in the strictest, most portable POSIX sh.
4-
# It makes zero assumptions about the shell's features.
5-
6-
# Use a simple, POSIX-compliant method for escaping JSON strings.
7+
# Very small, POSIX-compliant JSON string escaper
78
json_escape() {
8-
sed -e ':a;N;$!ba' -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\n/\\n/g' -e 's/\t/\\t/g' -e 's/\r/\\r/g'
9+
sed -e ':a;N;$!ba' \
10+
-e 's/\\/\\\\/g' \
11+
-e 's/"/\\"/g' \
12+
-e 's/\n/\\n/g' \
13+
-e 's/\t/\\t/g' \
14+
-e 's/\r/\\r/g'
915
}
1016

11-
# Exit immediately if no command is provided.
17+
# ---------- argument check --------------------------------------------------
18+
1219
if [ "$#" -eq 0 ]; then
13-
printf '{"exit_code": 127, "resource_usage": null, "stdout": "", "stderr": "Entrypoint Error: No command provided."}'
20+
printf '{"exit_code":127,"resource_usage":null,"stdout":"","stderr":"Entrypoint Error: No command provided."}'
1421
exit 0
1522
fi
1623

17-
# Create temporary files for stdout and stderr. Exit if mktemp fails.
24+
# ---------- temp files & timing --------------------------------------------
25+
1826
set -e
19-
OUT=$(mktemp)
20-
ERR=$(mktemp)
27+
OUT="$(mktemp -t out.XXXXXX)" || exit 1
28+
ERR="$(mktemp -t err.XXXXXX)" || exit 1
2129
trap 'rm -f "$OUT" "$ERR"' EXIT
2230
set +e
2331

24-
# Record start time using nanosecond precision (%s for seconds, %N for nanoseconds).
25-
START_TIME=$(date +%s.%N)
32+
START_TIME="$(date +%s.%N)"
33+
34+
# ---------- run user command in the background -----------------------------
2635

27-
# This subshell construct is the key. It isolates the user command.
28-
# A failure inside this subshell will not crash our main script.
2936
( "$@" >"$OUT" 2>"$ERR" ) &
3037
PID=$!
3138

39+
CLK_TCK="$(getconf CLK_TCK 2>/dev/null || printf '100')"
3240
PEAK_KB=0
3341
JIFS=0
3442

35-
# Loop while the process directory exists. This is the most reliable check.
36-
while [ -d "/proc/$PID" ]; do
37-
# Silence all errors from grep/cat to prevent log contamination from race conditions.
43+
# ---------- lightweight sampling loop --------------------------------------
44+
45+
# `kill -0` succeeds while the process is alive but does not send a signal.
46+
while kill -0 "$PID" 2>/dev/null; do
47+
# peak-RSS (VmHWM)
3848
CUR_KB=$(grep VmHWM "/proc/$PID/status" 2>/dev/null | awk '{print $2}')
3949
if [ -n "$CUR_KB" ] && [ "$CUR_KB" -gt "$PEAK_KB" ]; then
4050
PEAK_KB=$CUR_KB
4151
fi
4252

43-
# Use awk for arithmetic; it handles empty/malformed input without crashing the shell.
44-
CPU_JIFFIES=$(awk '{print $14 + $15}' "/proc/$PID/stat" 2>/dev/null)
53+
# user + sys CPU time (jiffies)
54+
CPU_JIFFIES=$(awk '{print $14+$15}' "/proc/$PID/stat" 2>/dev/null)
4555
if [ -n "$CPU_JIFFIES" ]; then
4656
JIFS=$CPU_JIFFIES
4757
fi
48-
sleep 0.05
58+
59+
# 5 ms sampling interval
60+
sleep 0.005
4961
done
5062

51-
# This will now work correctly because the parent script is not PID 1.
63+
# ---------- reap child & compute metrics -----------------------------------
64+
5265
wait "$PID"
5366
EXIT_CODE=$?
5467

55-
END_TIME=$(date +%s.%N)
56-
# Calculate elapsed time using floating-point math via awk, formatted to 6 decimal places.
57-
# The result is stored in ELAPSED_S, which the original JSON block uses.
58-
ELAPSED_S=$(printf '%s\n' "$END_TIME $START_TIME" | awk '{printf "%.6f", $1 - $2}')
68+
END_TIME="$(date +%s.%N)"
69+
ELAPSED_S=$(printf '%s\n' "$END_TIME $START_TIME" | awk '{printf "%.6f",$1-$2}')
5970

60-
# Defensively get clock ticks per second.
61-
CLK_TCK=$(getconf CLK_TCK 2>/dev/null || printf "100")
71+
# ---------- emit single-line JSON ------------------------------------------
6272

63-
OUT_JSON=$(cat "$OUT" | json_escape)
64-
ERR_JSON=$(cat "$ERR" | json_escape)
65-
66-
# Use the most robust printf format possible, passing all variables as arguments.
67-
# The final output has NO trailing newline. This is critical.
68-
json=$(cat <<EOF
73+
printf '%s' "$(cat <<EOF
6974
{
7075
"exit_code": ${EXIT_CODE:-1},
7176
"resource_usage": {
@@ -78,7 +83,4 @@ json=$(cat <<EOF
7883
"stderr": "$(cat "$ERR" | json_escape)"
7984
}
8085
EOF
81-
)
82-
83-
# final output – no trailing newline!
84-
printf '%s' "$json"
86+
)"

backend/app/services/execution_service.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,50 @@ async def get_k8s_resource_limits(self) -> Dict[str, Any]:
5353
"supported_runtimes": self.settings.SUPPORTED_RUNTIMES,
5454
}
5555

56-
async def _start_k8s_execution(
56+
async def _mark_running_when_scheduled(
5757
self,
58-
execution_id_str: str,
59-
script: str,
60-
lang: str,
61-
lang_version: str
58+
pod_name: str,
59+
execution_id: str,
60+
) -> None:
61+
"""
62+
Poll the K8s API until the Pod is actually Running, then mark the DB
63+
row as RUNNING. Stops polling after ~3 s to avoid useless work.
64+
"""
65+
try:
66+
for _ in range(30): # 30 × 0.1 s ≈ 3 s
67+
pod = await asyncio.to_thread(
68+
self.k8s_service.v1.read_namespaced_pod,
69+
name=pod_name,
70+
namespace=self.k8s_service.NAMESPACE,
71+
)
72+
if pod.status.phase == "Running":
73+
await self.execution_repo.update_execution(
74+
execution_id,
75+
ExecutionUpdate(status=ExecutionStatus.RUNNING)
76+
.model_dump(exclude_unset=True),
77+
)
78+
return
79+
await asyncio.sleep(0.1)
80+
except Exception as exc:
81+
logger.warning(
82+
f"Background poller for {execution_id} stopped "
83+
f"before RUNNING phase: {exc}"
84+
)
85+
86+
async def _start_k8s_execution(
87+
self,
88+
execution_id_str: str,
89+
script: str,
90+
lang: str,
91+
lang_version: str,
6292
) -> None:
93+
"""
94+
1. Ask KubernetesService to create the Pod.
95+
2. Fire-and-forget a poller that sets status=RUNNING exactly when
96+
the Pod becomes Running (not sooner).
97+
"""
98+
pod_name = f"execution-{execution_id_str}"
99+
63100
try:
64101
runtime_cfg = RUNTIME_REGISTRY[lang][lang_version]
65102
await self.k8s_service.create_execution_pod(
@@ -69,18 +106,24 @@ async def _start_k8s_execution(
69106
config_map_data={runtime_cfg.file_name: script},
70107
)
71108

72-
await self.execution_repo.update_execution(
73-
execution_id_str,
74-
ExecutionUpdate(status=ExecutionStatus.RUNNING).model_dump(exclude_unset=True)
109+
# Start background poller ⤵ — we do **not** await it here
110+
asyncio.create_task(
111+
self._mark_running_when_scheduled(pod_name, execution_id_str)
75112
)
76-
logger.info(f"K8s pod creation requested for execution {execution_id_str}, status set to RUNNING.")
113+
114+
logger.info(
115+
"K8s pod creation requested; waiting for Running phase",
116+
extra={"execution_id": execution_id_str},
117+
)
118+
77119
except Exception as e:
78120
error_message = f"Failed to request K8s pod creation: {str(e)}"
79121
logger.error(error_message, exc_info=True)
80122
await self.execution_repo.update_execution(
81123
execution_id_str,
82124
ExecutionUpdate(
83-
status=ExecutionStatus.ERROR, errors=error_message
125+
status=ExecutionStatus.ERROR,
126+
errors=error_message,
84127
).model_dump(exclude_unset=True),
85128
)
86129
raise IntegrationException(status_code=500, detail=error_message) from e

backend/app/services/kubernetes_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ async def create_execution_pod(
186186
)
187187
await self._create_config_map(config_map_body)
188188

189-
final_pod_command = ["/bin/sh", "/scripts/entrypoint.sh", *command]
189+
final_pod_command = ["/bin/sh", "/entry/entrypoint.sh", *command]
190190

191191
builder = PodManifestBuilder(
192192
execution_id=execution_id,

backend/app/services/pod_manifest_builder.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,50 +30,59 @@ def __init__(
3030

3131
def build(self) -> Dict[str, Any]:
3232
spec: Dict[str, Any] = {
33+
"initContainers": [
34+
{
35+
"name": "copy-script",
36+
"image": "busybox:1.36", # tiny & always available
37+
"command": ["/bin/sh", "-c",
38+
"cp /cfg/* /scripts/ && chmod 555 /scripts/*"],
39+
"volumeMounts": [
40+
{"name": "script-config", "mountPath": "/cfg", "readOnly": True},
41+
{"name": "script-volume", "mountPath": "/scripts"}
42+
]
43+
}
44+
],
3345
"containers": [
3446
{
3547
"name": "script-runner",
3648
"image": self.image,
3749
"imagePullPolicy": "IfNotPresent",
3850
"command": self.command,
39-
"args": [],
4051
"resources": {
41-
"limits": {"cpu": self.pod_cpu_limit, "memory": self.pod_memory_limit},
42-
"requests": {"cpu": self.pod_cpu_request, "memory": self.pod_memory_request},
52+
"limits": {"cpu": self.pod_cpu_limit,
53+
"memory": self.pod_memory_limit},
54+
"requests": {"cpu": self.pod_cpu_request,
55+
"memory": self.pod_memory_request},
4356
},
4457
"volumeMounts": [
45-
{"name": "script-volume", "mountPath": "/scripts"},
58+
{"name": "script-volume", "mountPath": "/scripts", "readOnly": True},
59+
{"name": "entrypoint-vol", "mountPath": "/entry", "readOnly": True},
4660
],
61+
"terminationMessagePolicy": "FallbackToLogsOnError",
4762
}
4863
],
4964
"volumes": [
50-
{
51-
"name": "script-volume",
52-
"configMap": {
53-
"name": self.config_map_name,
54-
"defaultMode": 0o755
55-
}
56-
},
65+
{"name": "script-volume", "emptyDir": {}},
66+
{"name": "script-config", "configMap": {"name": self.config_map_name}},
67+
{"name": "entrypoint-vol", "configMap": {"name": self.config_map_name}},
5768
],
5869
"restartPolicy": "Never",
59-
"activeDeadlineSeconds": self.pod_execution_timeout + 5,
70+
"activeDeadlineSeconds": self.pod_execution_timeout + 1,
6071
}
6172

6273
if self.priority_class_name:
6374
spec["priorityClassName"] = self.priority_class_name
6475

65-
pod_manifest: Dict[str, Any] = {
76+
return {
6677
"apiVersion": "v1",
6778
"kind": "Pod",
6879
"metadata": {
6980
"name": f"execution-{self.execution_id}",
7081
"namespace": self.namespace,
71-
"labels": {
72-
"app": "script-execution",
73-
"execution-id": self.execution_id,
74-
},
82+
"labels": {"app": "script-execution",
83+
"execution-id": self.execution_id},
7584
},
7685
"spec": spec,
7786
}
7887

79-
return pod_manifest
88+

0 commit comments

Comments
 (0)