DEVELOPMENT.md
1 # Development Guide - execd 2 3 This comprehensive guide explains how to work on `execd` as a contributor or maintainer. It covers environment setup, 4 development workflows, testing strategies, architectural patterns, and subsystem-specific implementation details. 5 6 ## Table of Contents 7 8 - [Getting Started](#getting-started) 9 - [Project Structure](#project-structure) 10 - [Coding Standards](#coding-standards) 11 - [Testing Strategy](#testing-strategy) 12 - [Subsystem Guides](#subsystem-guides) 13 - [Common Development Tasks](#common-development-tasks) 14 - [Debugging Techniques](#debugging-techniques) 15 - [Performance Optimization](#performance-optimization) 16 - [Contributing Guidelines](#contributing-guidelines) 17 - [Additional Resources](#additional-resources) 18 19 ## Getting Started 20 21 ### Prerequisites 22 23 #### Required Tools 24 25 - **Go 1.24+** - Match the version declared in `go.mod` 26 - **Git** - Version control 27 - **Make** - Build automation (optional but recommended) 28 29 #### Optional but Recommended 30 31 - **golangci-lint** - For comprehensive linting 32 - **Docker/Podman** - For containerized testing and deployment 33 - **Jupyter Server** - Required for integration tests with real kernels 34 - **VS Code/GoLand** - IDE with Go support 35 36 ### Initial Setup 37 38 ```bash 39 # Clone the repository 40 git clone https://github.com/alibaba/OpenSandbox.git 41 cd OpenSandbox/components/execd 42 43 # Download dependencies 44 go mod download 45 46 # Verify setup 47 go build -o bin/execd . 48 ``` 49 50 ## Project Structure 51 52 ### Project Structure Deep Dive 53 54 ``` 55 execd/ 56 ├── main.go # Application entry point 57 ├── go.mod # Go module definition 58 ├── Makefile # Build automation 59 ├── Dockerfile # Container image definition 60 │ 61 ├── pkg/ # Public packages 62 │ ├── flag/ # CLI flag parsing 63 │ ├── web/ # HTTP layer 64 │ │ ├── router.go # Route registration 65 │ │ ├── controller/ # Request handlers 66 │ │ └── model/ # API models 67 │ ├── runtime/ # Execution engine 68 │ │ ├── ctrl.go # Main controller 69 │ │ ├── jupyter.go # Jupyter execution 70 │ │ └── command.go # Shell command execution 71 │ ├── jupyter/ # Jupyter client 72 │ │ ├── client.go # HTTP/WebSocket client 73 │ │ ├── session/ # Session management 74 │ │ └── execute/ # Execution protocol 75 │ └── util/ # Utilities 76 │ 77 └── tests/ # Integration test scripts 78 ``` 79 80 ### Key Design Patterns 81 82 #### 1. Controller Pattern (pkg/web/controller) 83 84 Controllers are thin HTTP handlers that parse requests, validate, delegate to runtime, and stream responses via SSE. 85 86 #### 2. Runtime Controller Pattern (pkg/runtime) 87 88 The runtime controller dispatches requests to appropriate executors (Jupyter, Command, SQL) and manages session 89 lifecycle. 90 91 #### 3. Hook Pattern for Streaming 92 93 Execution results are streamed via hooks, allowing controllers to transform runtime events into SSE events without tight 94 coupling. 95 96 ## Coding Standards 97 98 ### Go Conventions 99 100 #### Formatting 101 102 **Always use `gofmt`** before committing: 103 104 ```bash 105 gofmt -w . 106 # or 107 make fmt 108 ``` 109 110 #### Import Organization 111 112 Three groups separated by blank lines: 113 114 ```go 115 import ( 116 // Standard library 117 "context" 118 "fmt" 119 120 // Third-party 121 "github.com/beego/beego/v2/core/logs" 122 123 // Internal 124 "github.com/alibaba/opensandbox/execd/pkg/runtime" 125 ) 126 ``` 127 128 #### Error Handling 129 130 Always handle errors explicitly: 131 132 ```go 133 // Good 134 result, err := someOperation() 135 if err != nil { 136 logs.Error("operation failed: %v", err) 137 return fmt.Errorf("failed to do something: %w", err) 138 } 139 140 // Bad - silent failure 141 result, _ := someOperation() 142 ``` 143 144 #### Logging 145 146 Use Beego's structured logger: 147 148 ```go 149 logs.Info("starting execution: sessionID=%s", sessionID) 150 logs.Warning("session busy: sessionID=%s", sessionID) 151 logs.Error("execution failed: error=%v", err) 152 logs.Debug("received event: type=%s", eventType) 153 ``` 154 155 ### Concurrency Best Practices 156 157 #### Use safego for goroutines 158 159 Always use `safego.Go` to prevent panics: 160 161 ```go 162 import "github.com/alibaba/opensandbox/internal/safego" 163 164 safego.Go(func() { 165 processInBackground() 166 }) 167 ``` 168 169 #### Context Propagation 170 171 Always respect context cancellation: 172 173 ```go 174 func (c *Controller) runCommand(ctx context.Context, req *ExecuteCodeRequest) error { 175 cmd := exec.CommandContext(ctx, "bash", "-c", req.Code) 176 177 go func() { 178 <-ctx.Done() 179 if cmd.Process != nil { 180 cmd.Process.Kill() 181 } 182 }() 183 184 return cmd.Run() 185 } 186 ``` 187 188 ## Testing Strategy 189 190 ### Unit Tests 191 192 Located in `*_test.go` files alongside source code. 193 194 **Example:** 195 196 ```go 197 func TestController_Execute_Python(t *testing.T) { 198 ctrl := NewController("http://jupyter:8888", "test-token") 199 200 req := &ExecuteCodeRequest{ 201 Language: Python, 202 Code: "print('hello')", 203 } 204 205 err := ctrl.Execute(req) 206 assert.NoError(t, err) 207 } 208 ``` 209 210 **Running Unit Tests:** 211 212 ```bash 213 go test ./pkg/... 214 # with coverage 215 go test -v -cover ./pkg/... 216 ``` 217 218 ### Integration Tests 219 220 Located in `*_integration_test.go`, require real dependencies. 221 222 **Running Integration Tests:** 223 224 ```bash 225 export JUPYTER_URL=http://localhost:8888 226 export JUPYTER_TOKEN=your-token 227 go test -v ./pkg/jupyter/... 228 ``` 229 230 ### Test Coverage 231 232 Check coverage: 233 234 ```bash 235 go test -coverprofile=coverage.out ./pkg/... 236 go tool cover -html=coverage.out -o coverage.html 237 ``` 238 239 **Coverage Goals:** 240 241 - Core packages (`pkg/runtime`, `pkg/jupyter`): > 80% 242 - Controllers (`pkg/web/controller`): > 70% 243 - Utilities (`pkg/util`): > 90% 244 245 ## Subsystem Guides 246 247 ### Working with Jupyter Integration 248 249 #### Architecture 250 251 ``` 252 pkg/jupyter/ 253 ├── client.go # Main client 254 ├── transport.go # Connection handling 255 ├── session/ # Session lifecycle 256 ├── execute/ # Execution protocol 257 └── auth/ # Authentication 258 ``` 259 260 #### Adding New Kernel Support 261 262 1. Define language in `pkg/runtime/language.go`: 263 264 ```go 265 const Ruby Language = "ruby" 266 ``` 267 268 2. Map to kernel in `pkg/runtime/jupyter.go` 269 270 3. Test with real kernel: 271 272 ```bash 273 # Install Ruby kernel 274 gem install iruby 275 iruby register --force 276 277 # Run test 278 export JUPYTER_URL=http://localhost:8888 279 go test -v ./pkg/jupyter/integration_test.go 280 ``` 281 282 #### Debugging Jupyter Communication 283 284 Run debug integration test: 285 286 ```bash 287 go test -v ./pkg/jupyter/debug_integration_test.go 288 ``` 289 290 This dumps complete HTTP request/response pairs. 291 292 ### Working with Command Execution 293 294 #### Key Implementation Details 295 296 **Process Group Management:** 297 298 ```go 299 cmd.SysProcAttr = &syscall.SysProcAttr{ 300 Setpgid: true, // Create new process group 301 } 302 ``` 303 304 This allows signal forwarding to all child processes: 305 306 ```go 307 syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM) 308 ``` 309 310 **Signal Forwarding:** 311 312 ```go 313 signals := make(chan os.Signal, 1) 314 signal.Notify(signals) 315 316 go func() { 317 for sig := range signals { 318 if sig != syscall.SIGCHLD && sig != syscall.SIGURG { 319 syscall.Kill(-cmd.Process.Pid, sig.(syscall.Signal)) 320 } 321 } 322 }() 323 ``` 324 325 **Stdout/Stderr Streaming:** 326 327 Commands write to temporary log files, which are tailed and streamed to hooks. 328 329 ## Common Development Tasks 330 331 ### Adding a New API Endpoint 332 333 1. **Define model** in `pkg/web/model/`: 334 335 ```go 336 type NewFeatureRequest struct { 337 Param1 string `json:"param1" validate:"required"` 338 Param2 int `json:"param2"` 339 } 340 ``` 341 342 2. **Add controller method** in `pkg/web/controller/`: 343 344 ```go 345 func (c *MyController) NewFeature() { 346 var req model.NewFeatureRequest 347 json.Unmarshal(c.Ctx.Input.RequestBody, &req) 348 349 // Business logic 350 result := processNewFeature(req) 351 352 c.Data["json"] = result 353 c.ServeJSON() 354 } 355 ``` 356 357 3. **Register route** in `pkg/web/router.go`: 358 359 ```go 360 myNamespace := web.NewNamespace("/my-feature", 361 web.NSRouter("", &controller.MyController{}, "post:NewFeature"), 362 ) 363 web.AddNamespace(myNamespace) 364 ``` 365 366 ### Adding Configuration Flag 367 368 1. **Declare in `pkg/flag/flags.go`:** 369 370 ```go 371 var NewFeatureTimeout time.Duration 372 ``` 373 374 2. **Parse in `pkg/flag/parser.go`:** 375 376 ```go 377 func InitFlags() { 378 flag.DurationVar(&NewFeatureTimeout, "new-feature-timeout", 30*time.Second, "Description") 379 380 // Parse environment variable 381 if env := os.Getenv("NEW_FEATURE_TIMEOUT"); env != "" { 382 if d, err := time.ParseDuration(env); err == nil { 383 NewFeatureTimeout = d 384 } 385 } 386 387 flag.Parse() 388 } 389 ``` 390 391 3. **Update README** with new flag documentation 392 393 ## Debugging Techniques 394 395 ### Local Debugging with Delve 396 397 ```bash 398 # Install delve 399 go install github.com/go-delve/delve/cmd/dlv@latest 400 401 # Start debugging 402 dlv debug . -- \ 403 --jupyter-host=http://localhost:8888 \ 404 --jupyter-token=test 405 406 # Set breakpoint 407 (dlv) break pkg/runtime/ctrl.go:57 408 (dlv) continue 409 ``` 410 411 ### Debugging SSE Streams 412 413 **Test with curl:** 414 415 ```bash 416 curl -N -H "x-access-token: dev" \ 417 -H "Content-Type: application/json" \ 418 -d '{"language":"python","code":"print(\"test\")"}' \ 419 http://localhost:44772/code 420 ``` 421 422 The `-N` flag disables buffering for real-time events. 423 424 **Debug in browser:** 425 426 ```javascript 427 const eventSource = new EventSource('/code'); 428 429 eventSource.addEventListener('stdout', (e) => { 430 console.log('stdout:', e.data); 431 }); 432 433 eventSource.addEventListener('error', (e) => { 434 console.error('error:', e.data); 435 }); 436 ``` 437 438 ### Performance Profiling 439 440 **CPU Profile:** 441 442 ```bash 443 # Add to main.go 444 import _ "net/http/pprof" 445 446 go func() { 447 http.ListenAndServe("localhost:6060", nil) 448 }() 449 450 # Collect profile 451 go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 452 ``` 453 454 **Memory Profile:** 455 456 ```bash 457 go tool pprof http://localhost:6060/debug/pprof/heap 458 ``` 459 460 **Goroutine Inspection:** 461 462 ```bash 463 curl http://localhost:6060/debug/pprof/goroutine?debug=2 464 ``` 465 466 ## Performance Optimization 467 468 ### Optimization Guidelines 469 470 1. **Profile before optimizing** - Use pprof to identify bottlenecks 471 2. **Benchmark changes** - Measure impact of optimizations 472 3. **Use `sync.Pool`** for frequently allocated objects 473 4. **Minimize allocations** in hot paths 474 5. **Buffer channels** appropriately 475 476 ### Example: Optimizing SSE Writer 477 478 **Before:** 479 480 ```go 481 func writeEvent(w http.ResponseWriter, event, data string) { 482 fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, data) 483 w.(http.Flusher).Flush() 484 } 485 ``` 486 487 **After:** 488 489 ```go 490 var bufPool = sync.Pool{ 491 New: func() interface{} { return new(bytes.Buffer) }, 492 } 493 494 func writeEvent(w http.ResponseWriter, event, data string) { 495 buf := bufPool.Get().(*bytes.Buffer) 496 buf.Reset() 497 defer bufPool.Put(buf) 498 499 buf.WriteString("event: ") 500 buf.WriteString(event) 501 buf.WriteString("\ndata: ") 502 buf.WriteString(data) 503 buf.WriteString("\n\n") 504 505 w.Write(buf.Bytes()) 506 w.(http.Flusher).Flush() 507 } 508 ``` 509 510 **Benchmark:** 511 512 ```go 513 func BenchmarkWriteEvent(b *testing.B) { 514 w := httptest.NewRecorder() 515 b.ResetTimer() 516 for i := 0; i < b.N; i++ { 517 writeEvent(w, "test", "data") 518 } 519 } 520 ``` 521 522 ## Contributing Guidelines 523 524 ### Pull Request Process 525 526 1. **Fork and clone** the repository 527 2. **Create feature branch** from `main` 528 3. **Implement changes** following coding standards 529 4. **Add tests** for new functionality 530 5. **Run all tests** and ensure they pass 531 6. **Update documentation** as needed 532 7. **Submit PR** with clear description 533 534 ### Code Review Standards 535 536 Reviewers check for: 537 538 - [ ] Correctness and functionality 539 - [ ] Test coverage 540 - [ ] Code style and formatting 541 - [ ] Documentation completeness 542 - [ ] Performance implications 543 - [ ] Security considerations 544 - [ ] Error handling 545 - [ ] Backwards compatibility 546 547 ### Release Checklist 548 549 Before releasing: 550 551 - [ ] All tests pass (unit, integration, e2e) 552 - [ ] Documentation updated (README, DEVELOPMENT, API docs) 553 - [ ] CHANGELOG updated with changes 554 - [ ] Version bumped appropriately (semver) 555 - [ ] Dependencies reviewed and updated 556 - [ ] Security scan passed 557 - [ ] Performance benchmarks run 558 - [ ] Docker image built and tested 559 560 ## Additional Resources 561 562 ### Useful Commands 563 564 ```bash 565 # Format all Go files 566 make fmt 567 568 # Run linter 569 make golint 570 571 # Run all tests 572 make test 573 574 # Build binary 575 make build 576 ``` 577 578 ### External Documentation 579 580 - [Beego Documentation](https://beego.wiki/) 581 - [Jupyter Kernel Protocol](https://jupyter-client.readthedocs.io/en/stable/messaging.html) 582 - [Go Best Practices](https://golang.org/doc/effective_go) 583 - [Server-Sent Events Spec](https://html.spec.whatwg.org/multipage/server-sent-events.html) 584 585 ### Getting Help 586 587 - **Issues**: Report bugs or request features on GitHub Issues 588 - **Discussions**: Ask questions in GitHub Discussions 589 - **Chat**: Join the OpenSandbox community chat 590 - **Documentation**: Check the wiki for detailed guides 591 592 --- 593 594 **Happy hacking!** Feel free to augment this guide with tips you discover along the way. For questions or suggestions, 595 open an issue or discussion on GitHub.