This comprehensive guide explains how to work on execd as a contributor or maintainer. It covers environment setup,
development workflows, testing strategies, architectural patterns, and subsystem-specific implementation details.
- Getting Started
- Project Structure
- Coding Standards
- Testing Strategy
- Subsystem Guides
- Common Development Tasks
- Debugging Techniques
- Performance Optimization
- Contributing Guidelines
- Additional Resources
- Go 1.24+ - Match the version declared in
go.mod - Git - Version control
- Make - Build automation (optional but recommended)
- golangci-lint - For comprehensive linting
- Docker/Podman - For containerized testing and deployment
- Jupyter Server - Required for integration tests with real kernels
- VS Code/GoLand - IDE with Go support
# Clone the repository
git clone https://github.com/alibaba/OpenSandbox.git
cd OpenSandbox/components/execd
# Download dependencies
go mod download
# Verify setup
go build -o bin/execd .execd/
├── main.go # Application entry point
├── go.mod # Go module definition
├── Makefile # Build automation
├── Dockerfile # Container image definition
│
├── pkg/ # Public packages
│ ├── flag/ # CLI flag parsing
│ ├── web/ # HTTP layer
│ │ ├── router.go # Route registration
│ │ ├── controller/ # Request handlers
│ │ └── model/ # API models
│ ├── runtime/ # Execution engine
│ │ ├── ctrl.go # Main controller
│ │ ├── jupyter.go # Jupyter execution
│ │ └── command.go # Shell command execution
│ ├── jupyter/ # Jupyter client
│ │ ├── client.go # HTTP/WebSocket client
│ │ ├── session/ # Session management
│ │ └── execute/ # Execution protocol
│ └── util/ # Utilities
│
└── tests/ # Integration test scripts
Controllers are thin HTTP handlers that parse requests, validate, delegate to runtime, and stream responses via SSE.
The runtime controller dispatches requests to appropriate executors (Jupyter, Command, SQL) and manages session lifecycle.
Execution results are streamed via hooks, allowing controllers to transform runtime events into SSE events without tight coupling.
Always use gofmt before committing:
gofmt -w .
# or
make fmtThree groups separated by blank lines:
import (
// Standard library
"context"
"fmt"
// Third-party
"github.com/beego/beego/v2/core/logs"
// Internal
"github.com/alibaba/opensandbox/execd/pkg/runtime"
)Always handle errors explicitly:
// Good
result, err := someOperation()
if err != nil {
logs.Error("operation failed: %v", err)
return fmt.Errorf("failed to do something: %w", err)
}
// Bad - silent failure
result, _ := someOperation()Use Beego's structured logger:
logs.Info("starting execution: sessionID=%s", sessionID)
logs.Warning("session busy: sessionID=%s", sessionID)
logs.Error("execution failed: error=%v", err)
logs.Debug("received event: type=%s", eventType)Always use safego.Go to prevent panics:
import "github.com/alibaba/opensandbox/execd/pkg/util/safego"
safego.Go(func() {
processInBackground()
})Always respect context cancellation:
func (c *Controller) runCommand(ctx context.Context, req *ExecuteCodeRequest) error {
cmd := exec.CommandContext(ctx, "bash", "-c", req.Code)
go func() {
<-ctx.Done()
if cmd.Process != nil {
cmd.Process.Kill()
}
}()
return cmd.Run()
}Located in *_test.go files alongside source code.
Example:
func TestController_Execute_Python(t *testing.T) {
ctrl := NewController("http://jupyter:8888", "test-token")
req := &ExecuteCodeRequest{
Language: Python,
Code: "print('hello')",
}
err := ctrl.Execute(req)
assert.NoError(t, err)
}Running Unit Tests:
go test ./pkg/...
# with coverage
go test -v -cover ./pkg/...Located in *_integration_test.go, require real dependencies.
Running Integration Tests:
export JUPYTER_URL=http://localhost:8888
export JUPYTER_TOKEN=your-token
go test -v ./pkg/jupyter/...Check coverage:
go test -coverprofile=coverage.out ./pkg/...
go tool cover -html=coverage.out -o coverage.htmlCoverage Goals:
- Core packages (
pkg/runtime,pkg/jupyter): > 80% - Controllers (
pkg/web/controller): > 70% - Utilities (
pkg/util): > 90%
pkg/jupyter/
├── client.go # Main client
├── transport.go # Connection handling
├── session/ # Session lifecycle
├── execute/ # Execution protocol
└── auth/ # Authentication
- Define language in
pkg/runtime/language.go:
const Ruby Language = "ruby"-
Map to kernel in
pkg/runtime/jupyter.go -
Test with real kernel:
# Install Ruby kernel
gem install iruby
iruby register --force
# Run test
export JUPYTER_URL=http://localhost:8888
go test -v ./pkg/jupyter/integration_test.goRun debug integration test:
go test -v ./pkg/jupyter/debug_integration_test.goThis dumps complete HTTP request/response pairs.
Process Group Management:
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // Create new process group
}This allows signal forwarding to all child processes:
syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)Signal Forwarding:
signals := make(chan os.Signal, 1)
signal.Notify(signals)
go func() {
for sig := range signals {
if sig != syscall.SIGCHLD && sig != syscall.SIGURG {
syscall.Kill(-cmd.Process.Pid, sig.(syscall.Signal))
}
}
}()Stdout/Stderr Streaming:
Commands write to temporary log files, which are tailed and streamed to hooks.
- Define model in
pkg/web/model/:
type NewFeatureRequest struct {
Param1 string `json:"param1" validate:"required"`
Param2 int `json:"param2"`
}- Add controller method in
pkg/web/controller/:
func (c *MyController) NewFeature() {
var req model.NewFeatureRequest
json.Unmarshal(c.Ctx.Input.RequestBody, &req)
// Business logic
result := processNewFeature(req)
c.Data["json"] = result
c.ServeJSON()
}- Register route in
pkg/web/router.go:
myNamespace := web.NewNamespace("/my-feature",
web.NSRouter("", &controller.MyController{}, "post:NewFeature"),
)
web.AddNamespace(myNamespace)- Declare in
pkg/flag/flags.go:
var NewFeatureTimeout time.Duration- Parse in
pkg/flag/parser.go:
func InitFlags() {
flag.DurationVar(&NewFeatureTimeout, "new-feature-timeout", 30*time.Second, "Description")
// Parse environment variable
if env := os.Getenv("NEW_FEATURE_TIMEOUT"); env != "" {
if d, err := time.ParseDuration(env); err == nil {
NewFeatureTimeout = d
}
}
flag.Parse()
}- Update README with new flag documentation
# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest
# Start debugging
dlv debug . -- \
--jupyter-host=http://localhost:8888 \
--jupyter-token=test
# Set breakpoint
(dlv) break pkg/runtime/ctrl.go:57
(dlv) continueTest with curl:
curl -N -H "x-access-token: dev" \
-H "Content-Type: application/json" \
-d '{"language":"python","code":"print(\"test\")"}' \
http://localhost:44772/codeThe -N flag disables buffering for real-time events.
Debug in browser:
const eventSource = new EventSource('/code');
eventSource.addEventListener('stdout', (e) => {
console.log('stdout:', e.data);
});
eventSource.addEventListener('error', (e) => {
console.error('error:', e.data);
});CPU Profile:
# Add to main.go
import _ "net/http/pprof"
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
# Collect profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30Memory Profile:
go tool pprof http://localhost:6060/debug/pprof/heapGoroutine Inspection:
curl http://localhost:6060/debug/pprof/goroutine?debug=2- Profile before optimizing - Use pprof to identify bottlenecks
- Benchmark changes - Measure impact of optimizations
- Use
sync.Poolfor frequently allocated objects - Minimize allocations in hot paths
- Buffer channels appropriately
Before:
func writeEvent(w http.ResponseWriter, event, data string) {
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, data)
w.(http.Flusher).Flush()
}After:
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func writeEvent(w http.ResponseWriter, event, data string) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
buf.WriteString("event: ")
buf.WriteString(event)
buf.WriteString("\ndata: ")
buf.WriteString(data)
buf.WriteString("\n\n")
w.Write(buf.Bytes())
w.(http.Flusher).Flush()
}Benchmark:
func BenchmarkWriteEvent(b *testing.B) {
w := httptest.NewRecorder()
b.ResetTimer()
for i := 0; i < b.N; i++ {
writeEvent(w, "test", "data")
}
}- Fork and clone the repository
- Create feature branch from
main - Implement changes following coding standards
- Add tests for new functionality
- Run all tests and ensure they pass
- Update documentation as needed
- Submit PR with clear description
Reviewers check for:
- Correctness and functionality
- Test coverage
- Code style and formatting
- Documentation completeness
- Performance implications
- Security considerations
- Error handling
- Backwards compatibility
Before releasing:
- All tests pass (unit, integration, e2e)
- Documentation updated (README, DEVELOPMENT, API docs)
- CHANGELOG updated with changes
- Version bumped appropriately (semver)
- Dependencies reviewed and updated
- Security scan passed
- Performance benchmarks run
- Docker image built and tested
# Format all Go files
make fmt
# Run linter
make golint
# Run all tests
make test
# Build binary
make build- Issues: Report bugs or request features on GitHub Issues
- Discussions: Ask questions in GitHub Discussions
- Chat: Join the OpenSandbox community chat
- Documentation: Check the wiki for detailed guides
Happy hacking! Feel free to augment this guide with tips you discover along the way. For questions or suggestions, open an issue or discussion on GitHub.