/ components / execd / DEVELOPMENT.md
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.