Skip to content

Commit 3ab0f7e

Browse files
fix: publish the active runner environment through the guarded accessor (#27)
Code, Tooling and the REPL read pythonservice.Service.ActiveEnv on their own gRPC goroutines while this agent published it from Init and cleared it from Stop. runnerMu ordered the writers against each other but bought nothing on the read side, and the field lives in another module, so the fix had to land there first: service-python v0.0.60 unexports it behind ActiveEnv/SetActiveEnv under an RWMutex. Both call sites now go through the setter, which is what gives a reader arriving after teardown the clean nil fallback instead of an environment whose docker client is already closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent eb917be commit 3ab0f7e

4 files changed

Lines changed: 170 additions & 8 deletions

File tree

‎active_env_test.go‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"sync"
9+
"testing"
10+
11+
runtimev0 "github.com/codefly-dev/core/generated/go/codefly/services/runtime/v0"
12+
"github.com/codefly-dev/core/resources"
13+
"github.com/codefly-dev/core/shared"
14+
pythoncode "github.com/codefly-dev/service-python/pkg/code"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// activeEnvRuntime is a native-mode Runtime ready to build a runner
19+
// environment: Location for the cache directory, a runtime context, and a
20+
// Python source tree for Code to analyze.
21+
func activeEnvRuntime(t *testing.T) (*Runtime, string) {
22+
t.Helper()
23+
24+
runtime := NewRuntime(NewService())
25+
runtime.Location = t.TempDir()
26+
runtime.Logger = runtime.Wool
27+
runtime.Base.Runtime.RuntimeContext = resources.NewRuntimeContextNative()
28+
runtime.Identity = &resources.ServiceIdentity{
29+
Name: "active-env",
30+
Module: "mod",
31+
Workspace: "test",
32+
WorkspacePath: runtime.Location,
33+
}
34+
runtime.Environment = shared.Must(resources.LocalEnvironment().Proto())
35+
36+
source := filepath.Join(runtime.Location, "code")
37+
require.NoError(t, os.MkdirAll(source, 0o700))
38+
require.NoError(t, os.WriteFile(filepath.Join(source, "sample.py"),
39+
[]byte("def answer():\n return 42\n"), 0o600))
40+
runtime.Service.SourceLocation = source
41+
42+
return runtime, source
43+
}
44+
45+
// TestActiveEnvironmentFollowsTheRunnerLifecycle pins the contract this agent
46+
// owes the generic layer: what it builds is what Code, Tooling and the REPL
47+
// resolve, and a teardown hands them nil rather than an environment whose
48+
// docker client is already closed.
49+
func TestActiveEnvironmentFollowsTheRunnerLifecycle(t *testing.T) {
50+
ctx := context.Background()
51+
runtime, _ := activeEnvRuntime(t)
52+
53+
require.Nil(t, runtime.FastAPI.Service.ActiveEnv(),
54+
"nothing may be published before a runner environment exists")
55+
56+
require.NoError(t, runtime.CreateRunnerEnvironment(ctx))
57+
require.Same(t, runtime.runnerEnvironment, runtime.FastAPI.Service.ActiveEnv(),
58+
"Code must resolve the environment this agent actually runs in")
59+
60+
_, err := runtime.Stop(ctx, &runtimev0.StopRequest{})
61+
require.NoError(t, err)
62+
require.Nil(t, runtime.FastAPI.Service.ActiveEnv(),
63+
"a shut-down environment must not stay reachable from Code")
64+
}
65+
66+
// TestCodeIsSafeWhileTheRunnerLifecycleChurns runs the generic Code handler —
67+
// the same instance main.go wires up — against a runtime publishing and
68+
// clearing its environment underneath it. Code and the lifecycle RPCs are
69+
// concurrent gRPC handlers, and the field they meet on lives in another
70+
// module, so this is the only side that can drive both at once.
71+
//
72+
// The publisher holds runnerMu across CreateRunnerEnvironment because the
73+
// runtime-local runnerEnvironment field is ordered by that lock; the point
74+
// here is the published value the other module reads.
75+
func TestCodeIsSafeWhileTheRunnerLifecycleChurns(t *testing.T) {
76+
if _, err := exec.LookPath("python3"); err != nil {
77+
t.Skip("python3 not on PATH")
78+
}
79+
ctx := context.Background()
80+
runtime, source := activeEnvRuntime(t)
81+
code := pythoncode.New(runtime.FastAPI.Service)
82+
83+
done := make(chan struct{})
84+
var lifecycle sync.WaitGroup
85+
var lifecycleErr error
86+
lifecycle.Add(1)
87+
go func() {
88+
defer lifecycle.Done()
89+
for {
90+
select {
91+
case <-done:
92+
return
93+
default:
94+
}
95+
runtime.runnerMu.Lock()
96+
err := runtime.CreateRunnerEnvironment(ctx)
97+
runtime.runnerMu.Unlock()
98+
if err == nil {
99+
_, err = runtime.Stop(ctx, &runtimev0.StopRequest{})
100+
}
101+
if err != nil {
102+
lifecycleErr = err
103+
return
104+
}
105+
}
106+
}()
107+
108+
for i := 0; i < 10; i++ {
109+
graph := code.ComputePythonCallGraph(ctx, source)
110+
require.Empty(t, graph.Error, "call graph %d must answer whatever the lifecycle is doing", i)
111+
}
112+
close(done)
113+
lifecycle.Wait()
114+
require.NoError(t, lifecycleErr, "the runner lifecycle must keep working under a concurrent reader")
115+
}

‎go.mod‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/codefly-dev/service-python-fastapi
33
go 1.27.0
44

55
require (
6-
github.com/codefly-dev/core v0.3.8
7-
github.com/codefly-dev/service-python v0.0.52
6+
github.com/codefly-dev/core v0.3.11
7+
github.com/codefly-dev/service-python v0.0.60
88
github.com/stretchr/testify v1.11.1
99
google.golang.org/grpc v1.83.0
1010
gopkg.in/yaml.v3 v3.0.1
@@ -33,6 +33,7 @@ require (
3333
github.com/containerd/errdefs/pkg v0.3.0 // indirect
3434
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
3535
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
36+
github.com/dekobon/tree-sitter-groovy v0.2.2 // indirect
3637
github.com/distribution/reference v0.6.0 // indirect
3738
github.com/docker/docker v28.5.2+incompatible // indirect
3839
github.com/docker/go-connections v0.8.1 // indirect
@@ -83,6 +84,7 @@ require (
8384
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
8485
github.com/mattn/go-colorable v0.1.15 // indirect
8586
github.com/mattn/go-isatty v0.0.24 // indirect
87+
github.com/mattn/go-pointer v0.0.1 // indirect
8688
github.com/mattn/go-runewidth v0.0.28 // indirect
8789
github.com/mitchellh/mapstructure v1.5.0 // indirect
8890
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -103,6 +105,15 @@ require (
103105
github.com/skeema/knownhosts v1.3.2 // indirect
104106
github.com/tklauser/go-sysconf v0.4.0 // indirect
105107
github.com/tklauser/numcpus v0.12.0 // indirect
108+
github.com/tree-sitter-grammars/tree-sitter-kotlin v1.1.0 // indirect
109+
github.com/tree-sitter/go-tree-sitter v0.25.0 // indirect
110+
github.com/tree-sitter/tree-sitter-c-sharp v0.23.5 // indirect
111+
github.com/tree-sitter/tree-sitter-go v0.25.0 // indirect
112+
github.com/tree-sitter/tree-sitter-java v0.23.5 // indirect
113+
github.com/tree-sitter/tree-sitter-javascript v0.25.0 // indirect
114+
github.com/tree-sitter/tree-sitter-python v0.25.0 // indirect
115+
github.com/tree-sitter/tree-sitter-rust v0.24.2 // indirect
116+
github.com/tree-sitter/tree-sitter-typescript v0.23.2 // indirect
106117
github.com/xanzy/ssh-agent v0.3.3 // indirect
107118
github.com/xlab/treeprint v1.2.0 // indirect
108119
github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect

‎go.sum‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
4141
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
4242
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
4343
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
44-
github.com/codefly-dev/core v0.3.8 h1:iqXrad1zwpX1CzAKr53TeuYLWHV8B5QkhHVV6unb9Os=
45-
github.com/codefly-dev/core v0.3.8/go.mod h1:4V8BP4F5mnRDZvylhC9P5N7rfifpAma5n3dW4YD+gtQ=
44+
github.com/codefly-dev/core v0.3.11 h1:FPYNQ3MysUeuZmXISBl7G2PEkzijE0wBWen2sF8pqMc=
45+
github.com/codefly-dev/core v0.3.11/go.mod h1:4V8BP4F5mnRDZvylhC9P5N7rfifpAma5n3dW4YD+gtQ=
4646
github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ=
4747
github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg=
48-
github.com/codefly-dev/service-python v0.0.52 h1:DY5IBBAmlna4Ll5XL8md5QvOqeB6BGA7mfPQsqfC1mw=
49-
github.com/codefly-dev/service-python v0.0.52/go.mod h1:qqcC3IWcnAYxK8Lst1ZrrrvsWL1+9/UhhB+mSvvWye0=
48+
github.com/codefly-dev/service-python v0.0.60 h1:gOwWBGtb9Kvy7tPc4Xmj8H2Ws44aaP30KmsyJAKnZEU=
49+
github.com/codefly-dev/service-python v0.0.60/go.mod h1:GHxhihx2UsWWOoXcFBKBfD7kz5XjyI6x0sQV5VRjQ5s=
5050
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
5151
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
5252
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -59,6 +59,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
5959
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6060
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
6161
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
62+
github.com/dekobon/tree-sitter-groovy v0.2.2 h1:ppghxHjtcQQakgjVStZN6lTFjjh5kluT1Y1dUQKfn4w=
63+
github.com/dekobon/tree-sitter-groovy v0.2.2/go.mod h1:zmwPJRzx9mn9Ca9qEa20FKgg9NukaZF0hUL4gF6zCbg=
6264
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
6365
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
6466
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
@@ -190,6 +192,8 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy
190192
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
191193
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
192194
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
195+
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
196+
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
193197
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
194198
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
195199
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -259,6 +263,38 @@ github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRU
259263
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
260264
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
261265
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
266+
github.com/tree-sitter-grammars/tree-sitter-kotlin v1.1.0 h1:SWIUDASa+WPhDDem1U5IJpYwQEezkqXrUI61OcnORzM=
267+
github.com/tree-sitter-grammars/tree-sitter-kotlin v1.1.0/go.mod h1:eH+flFf3QOa9c9BY9g3Bz02F7zTq30kGIG3cgB0lSlI=
268+
github.com/tree-sitter/go-tree-sitter v0.25.0 h1:sx6kcg8raRFCvc9BnXglke6axya12krCJF5xJ2sftRU=
269+
github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU=
270+
github.com/tree-sitter/tree-sitter-c v0.23.4 h1:nBPH3FV07DzAD7p0GfNvXM+Y7pNIoPenQWBpvM++t4c=
271+
github.com/tree-sitter/tree-sitter-c v0.23.4/go.mod h1:MkI5dOiIpeN94LNjeCp8ljXN/953JCwAby4bClMr6bw=
272+
github.com/tree-sitter/tree-sitter-c-sharp v0.23.5 h1:EeUc2WJE5G1pD6YEqera2yVHYeroeR+/MakKX2a+0xQ=
273+
github.com/tree-sitter/tree-sitter-c-sharp v0.23.5/go.mod h1:H7/aFm5vR1A8Yn5VIOfLWPdlKuJsMgZ5eDmaJdv8bY0=
274+
github.com/tree-sitter/tree-sitter-cpp v0.23.4 h1:LaWZsiqQKvR65yHgKmnaqA+uz6tlDJTJFCyFIeZU/8w=
275+
github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8=
276+
github.com/tree-sitter/tree-sitter-embedded-template v0.23.2 h1:nFkkH6Sbe56EXLmZBqHHcamTpmz3TId97I16EnGy4rg=
277+
github.com/tree-sitter/tree-sitter-embedded-template v0.23.2/go.mod h1:HNPOhN0qF3hWluYLdxWs5WbzP/iE4aaRVPMsdxuzIaQ=
278+
github.com/tree-sitter/tree-sitter-go v0.25.0 h1:cEB0Q3LHgZtS+ECHx9wcP7AwzoOddJFQCVmytX42cVU=
279+
github.com/tree-sitter/tree-sitter-go v0.25.0/go.mod h1:Jrx8QqYN0v7npv1fJRH1AznddllYiCMUChtVjxPK040=
280+
github.com/tree-sitter/tree-sitter-html v0.23.2 h1:1UYDV+Yd05GGRhVnTcbP58GkKLSHHZwVaN+lBZV11Lc=
281+
github.com/tree-sitter/tree-sitter-html v0.23.2/go.mod h1:gpUv/dG3Xl/eebqgeYeFMt+JLOY9cgFinb/Nw08a9og=
282+
github.com/tree-sitter/tree-sitter-java v0.23.5 h1:J9YeMGMwXYlKSP3K4Us8CitC6hjtMjqpeOf2GGo6tig=
283+
github.com/tree-sitter/tree-sitter-java v0.23.5/go.mod h1:NRKlI8+EznxA7t1Yt3xtraPk1Wzqh3GAIC46wxvc320=
284+
github.com/tree-sitter/tree-sitter-javascript v0.25.0 h1:ZkWETb66/w8cc13yhfnNuHOLDQWl3BnKlH6f9AdR88c=
285+
github.com/tree-sitter/tree-sitter-javascript v0.25.0/go.mod h1:lmGD1EJdCA+v0S1u2fFgepMg/opzSg/4pgFym2FPGAs=
286+
github.com/tree-sitter/tree-sitter-json v0.24.8 h1:tV5rMkihgtiOe14a9LHfDY5kzTl5GNUYe6carZBn0fQ=
287+
github.com/tree-sitter/tree-sitter-json v0.24.8/go.mod h1:F351KK0KGvCaYbZ5zxwx/gWWvZhIDl0eMtn+1r+gQbo=
288+
github.com/tree-sitter/tree-sitter-php v0.23.11 h1:iHewsLNDmznh8kgGyfWfujsZxIz1YGbSd2ZTEM0ZiP8=
289+
github.com/tree-sitter/tree-sitter-php v0.23.11/go.mod h1:T/kbfi+UcCywQfUNAJnGTN/fMSUjnwPXA8k4yoIks74=
290+
github.com/tree-sitter/tree-sitter-python v0.25.0 h1:O6XD9v8U1LOcRc3cNj9nM7XufrtEBezE6VrpRrHZDf0=
291+
github.com/tree-sitter/tree-sitter-python v0.25.0/go.mod h1:cpdthSy/Yoa28aJFBscFHlGiU+cnSiSh1kuDVtI8YeM=
292+
github.com/tree-sitter/tree-sitter-ruby v0.23.1 h1:T/NKHUA+iVbHM440hFx+lzVOzS4dV6z8Qw8ai+72bYo=
293+
github.com/tree-sitter/tree-sitter-ruby v0.23.1/go.mod h1:kUS4kCCQloFcdX6sdpr8p6r2rogbM6ZjTox5ZOQy8cA=
294+
github.com/tree-sitter/tree-sitter-rust v0.24.2 h1:NL4nF67ib21RMzzfvkmXlVwe45vvhW10DVyO+D0z/W0=
295+
github.com/tree-sitter/tree-sitter-rust v0.24.2/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI=
296+
github.com/tree-sitter/tree-sitter-typescript v0.23.2 h1:/Odvphn18PniVixb9e97X0DbNVsU6Qocv9mfkyzdXwU=
297+
github.com/tree-sitter/tree-sitter-typescript v0.23.2/go.mod h1:zjzMXT/Ulffel2xfOcAkQQkiAkmgnbtPGlFQw/5X4xA=
262298
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
263299
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
264300
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=

‎runtime.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func (s *Runtime) CreateRunnerEnvironment(ctx context.Context) error {
259259
s.runnerEnvironment.WithEnvironmentVariables(ctx, resources.Env("PYTHONUNBUFFERED", 1))
260260
// Share with Code / Tooling so AST analysis, grep, uv sync follow
261261
// whatever mode the plugin is in.
262-
s.FastAPI.Service.ActiveEnv = s.runnerEnvironment
262+
s.FastAPI.Service.SetActiveEnv(s.runnerEnvironment)
263263
return nil
264264
}
265265

@@ -816,7 +816,7 @@ func (s *Runtime) endExecution(ctx context.Context) (bool, error) {
816816
// a fresh one rather than hand Code and the REPL a dead handle.
817817
released = true
818818
s.runnerEnvironment = nil
819-
s.FastAPI.Service.ActiveEnv = nil
819+
s.FastAPI.Service.SetActiveEnv(nil)
820820
}
821821
}
822822
return released, errors.Join(errs...)

0 commit comments

Comments
 (0)