Skip to content

Commit addc9c0

Browse files
committed
Cache styx tool outputs across full-pipeline CI runs
Wrap the session runner with styxcache's CachingRunner on the self-hosted runner (gated on RBC_STYXCACHE_DIR) so ANTs/AFNI/FSL invocations reuse outputs across runs when inputs, env, and container digest match. Also moves the pytest basetemp off /dev/shm onto disk to free up RAM for the tools themselves. CachingRunner doesn't proxy base-runner attributes, so rbc.core.niwrap helpers that read/mutate data_dir/uid/execution_counter would break; _AttrProxyCachingRunner forwards attr reads and writes to self.base. The workflow provisions /var/lib/rbc-ci/styxcache, purges stale .incoming staging dirs, enforces a 50GB soft cap, and reports size and entry count to the job summary.
1 parent 548d95b commit addc9c0

4 files changed

Lines changed: 168 additions & 4 deletions

File tree

.github/workflows/test_full.yaml

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ on:
1515
description: Keep intermediate files after running tests
1616
default: false
1717
type: boolean
18+
styxcache_dir:
19+
description: Override persistent styxcache directory on the self-hosted runner
20+
default: ""
21+
type: string
1822

1923
concurrency:
2024
group: ${{ github.workflow }}-${{ github.ref }}
@@ -23,6 +27,9 @@ concurrency:
2327
jobs:
2428
test:
2529
runs-on: arcana
30+
env:
31+
STYXCACHE_DIR: ${{ inputs.styxcache_dir || '/var/lib/rbc-ci/styxcache' }}
32+
STYXCACHE_MAX_GB: "50"
2633
steps:
2734
- uses: actions/checkout@v6
2835
- uses: astral-sh/setup-uv@v8.0.0
@@ -31,20 +38,50 @@ jobs:
3138
enable-cache: true # not automatic on self-hosted runners
3239
- run: uv sync
3340

41+
- name: Prepare styx cache
42+
shell: bash
43+
run: |
44+
mkdir -p "$STYXCACHE_DIR"
45+
# Purge stale staging dirs from crashed previous runs (safe: atomic-rename commits)
46+
rm -rf "$STYXCACHE_DIR/.incoming" 2>/dev/null || true
47+
48+
size_kb=$(du -sk "$STYXCACHE_DIR" 2>/dev/null | awk '{print $1}')
49+
size_kb=${size_kb:-0}
50+
cap_kb=$(( STYXCACHE_MAX_GB * 1024 * 1024 ))
51+
if [ "$size_kb" -gt "$cap_kb" ]; then
52+
echo "Cache size ${size_kb}KB exceeds cap ${cap_kb}KB; evicting oldest shards"
53+
find "$STYXCACHE_DIR" -mindepth 2 -maxdepth 2 -type d -printf '%T@ %p\n' \
54+
| sort -n \
55+
| while read -r _ path; do
56+
rm -rf "$path"
57+
cur=$(du -sk "$STYXCACHE_DIR" 2>/dev/null | awk '{print $1}')
58+
[ "${cur:-0}" -le "$cap_kb" ] && break
59+
done
60+
fi
61+
62+
{
63+
echo "## styxcache"
64+
echo "- dir: \`$STYXCACHE_DIR\`"
65+
echo "- size before: $(du -sh "$STYXCACHE_DIR" 2>/dev/null | cut -f1)"
66+
} >> "$GITHUB_STEP_SUMMARY"
67+
3468
- name: Run all tests
3569
shell: bash
3670
run: |
37-
# Clean up stale tmpfs dirs from previous crashed runs
38-
find /dev/shm -maxdepth 1 -name 'rbc_test_*' -mmin +120 -exec rm -rf {} + 2>/dev/null || true
71+
tmp_root="${RUNNER_TEMP:-/var/lib/rbc-ci/tmp}"
72+
mkdir -p "$tmp_root"
73+
# Clean up stale job tmp dirs from previous crashed runs
74+
find "$tmp_root" -maxdepth 1 -name 'rbc_test_*' -mmin +120 -exec rm -rf {} + 2>/dev/null || true
3975
40-
export JOB_TMP="/dev/shm/rbc_test_$(date +%s%N)"
76+
export JOB_TMP="$tmp_root/rbc_test_$(date +%s%N)"
4177
mkdir -p $JOB_TMP
4278
echo "JOB_TMP=$JOB_TMP" >> $GITHUB_ENV
4379
echo "Job temp directory: $JOB_TMP"
4480
4581
export PYTEST_CACHE_DIR="$JOB_TMP/.pytest_cache"
4682
export PYTHONPYCACHEPREFIX="$JOB_TMP/__pycache__"
4783
export COVERAGE_FILE="$JOB_TMP/.coverage"
84+
export RBC_STYXCACHE_DIR="$STYXCACHE_DIR"
4885
4986
uv run pytest \
5087
-n 8 \
@@ -87,6 +124,17 @@ jobs:
87124
--show-missing \
88125
--format=markdown >> $GITHUB_STEP_SUMMARY
89126
127+
- name: styxcache summary
128+
if: always()
129+
shell: bash
130+
run: |
131+
size=$(du -sh "$STYXCACHE_DIR" 2>/dev/null | cut -f1)
132+
entries=$(find "$STYXCACHE_DIR" -mindepth 2 -maxdepth 2 -type d 2>/dev/null | wc -l)
133+
{
134+
echo "- size after: ${size:-0}"
135+
echo "- entries: ${entries:-0}"
136+
} >> "$GITHUB_STEP_SUMMARY"
137+
90138
- name: Cleanup
91139
if: ${{ always() && github.event.inputs.no_cleanup != true }}
92140
shell: bash

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ dev = [
2828
"pytest-cov>=7.0.0",
2929
"ruff>=0.8.1",
3030
"deptry>=0.23.0",
31-
"pytest-xdist[psutil]>=3.8.0"
31+
"pytest-xdist[psutil]>=3.8.0",
32+
"styxcache>=0.1.0"
3233
]
3334
docs = ["pdoc>=15.0.0"]
3435

tests/full_pipeline/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import niwrap
1212
import pytest
13+
from styxcache import CachePolicy, CachingRunner
14+
from styxcache.backends import docker_digest_resolver, podman_digest_resolver
1315
from styxpodman import PodmanRunner
1416

1517
from rbc.core.niwrap import resolve_runner
@@ -29,6 +31,21 @@
2931
MANIFEST_PATH = Path(__file__).parent / ".last_run.json"
3032

3133

34+
class _AttrProxyCachingRunner(CachingRunner):
35+
# CachingRunner doesn't proxy base-runner attrs, but rbc.core.niwrap
36+
# reads/mutates data_dir, uid, execution_counter on the global runner.
37+
_OWN_ATTRS = frozenset({"base", "store", "policy"})
38+
39+
def __getattr__(self, name: str) -> object:
40+
return getattr(self.__dict__["base"], name)
41+
42+
def __setattr__(self, name: str, value: object) -> None:
43+
if "base" not in self.__dict__ or name in self._OWN_ATTRS:
44+
super().__setattr__(name, value)
45+
else:
46+
setattr(self.__dict__["base"], name, value)
47+
48+
3249
class PipelineData(NamedTuple):
3350
"""Shared outputs from the anatomical + functional preprocessing chain."""
3451

@@ -69,6 +86,21 @@ def _niwrap_session_runner(
6986
runner.data_dir = data_dir
7087
logger = logging.getLogger(runner.logger_name)
7188
logger.setLevel(logging.DEBUG)
89+
90+
cache_dir = os.environ.get("RBC_STYXCACHE_DIR")
91+
if cache_dir and runner_type in {"docker", "podman"}:
92+
resolver = (
93+
docker_digest_resolver
94+
if runner_type == "docker"
95+
else podman_digest_resolver
96+
)
97+
wrapped = _AttrProxyCachingRunner(
98+
base=runner,
99+
cache_dir=cache_dir,
100+
policy=CachePolicy(image_digest=resolver),
101+
)
102+
niwrap.set_global_runner(wrapped)
103+
return wrapped
72104
return runner
73105

74106

0 commit comments

Comments
 (0)