codegen

package
v0.0.0-...-8929951 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 30, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package codegen provides protobuf code generation for 15+ programming languages.

Overview

This package implements the compilation system that transforms protobuf definitions (.proto files) into language-specific code. It supports two compilation backends: v1 (legacy) for simple direct protoc invocation, and v2 (orchestrator) for Docker-based compilation with advanced features like caching, dependency management, and parallel builds.

Architecture

The code generation system consists of six major components:

  1. Orchestrator (pkg/codegen/orchestrator): Coordinates compilation workflow
  2. Languages (pkg/codegen/languages): Language specifications and plugin management
  3. Docker (pkg/codegen/docker): Container-based protoc execution
  4. Cache (pkg/codegen/cache): Two-tier caching (in-memory L1 + Redis L2)
  5. Packages (pkg/codegen/packages): Package manager file generation (go.mod, setup.py, etc.)
  6. Artifacts (pkg/codegen/artifacts): S3 storage for compiled artifacts

Compilation System

Spoke uses an orchestrator-based compilation system with Docker containers:

  • Docker containers with pre-installed protoc and plugins
  • Automatic dependency resolution and compilation
  • Two-tier caching (L1 in-memory, L2 Redis)
  • S3 artifact storage for compiled code
  • Parallel compilation for multiple languages
  • Package manager file generation

Supported Languages

The system supports 15+ languages with varying maturity levels:

Language       Status    gRPC  Package Manager
--------       ------    ----  ---------------
Go             Stable    Yes   go.mod
Python         Stable    Yes   setup.py, requirements.txt
Java           Stable    Yes   pom.xml, build.gradle
C++            Stable    Yes   CMakeLists.txt
C#             Stable    Yes   .csproj
TypeScript     Beta      Yes   package.json
JavaScript     Beta      Yes   package.json
Rust           Beta      Yes   Cargo.toml
Kotlin         Beta      Yes   build.gradle.kts
Swift          Beta      Yes   Package.swift
Dart           Beta      Yes   pubspec.yaml
Ruby           Alpha     Yes   Gemfile
PHP            Alpha     No    composer.json
Scala          Alpha     Yes   build.sbt
Objective-C    Alpha     Yes   Podfile

Language specifications are registered in pkg/codegen/languages/registry.go.

Basic Usage

Using the orchestrator (v2):

import (
	"context"
	"github.com/platinummonkey/spoke/pkg/codegen/orchestrator"
)

// Create orchestrator
config := orchestrator.DefaultConfig()
config.EnableCache = true
config.S3Bucket = "spoke-artifacts"
orch, err := orchestrator.NewOrchestrator(config)
if err != nil {
	log.Fatal(err)
}
defer orch.Close()

// Compile for Go
req := &orchestrator.CompileRequest{
	ModuleName:  "user-service",
	Version:     "v1.0.0",
	Language:    "go",
	ProtoFiles: []codegen.ProtoFile{
		{Path: "user.proto", Content: protoContent},
	},
	IncludeGRPC: true,
	Options: map[string]string{
		"go_package": "github.com/example/user",
	},
}

result, err := orch.CompileSingle(context.Background(), req)
if err != nil {
	log.Fatal(err)
}

if result.Success {
	fmt.Printf("Generated %d files\n", len(result.GeneratedFiles))
	for _, file := range result.GeneratedFiles {
		fmt.Printf("  %s (%d bytes)\n", file.Path, file.Size)
	}
}

Compile for multiple languages in parallel:

results, err := orch.CompileAll(ctx, req, []string{"go", "python", "typescript"})
for _, result := range results {
	if result.Success {
		fmt.Printf("%s: ✓ (%v)\n", result.Language, result.Duration)
	} else {
		fmt.Printf("%s: ✗ %s\n", result.Language, result.Error)
	}
}

Compilation Flow

The v2 orchestrator follows this workflow:

1. Validate Request: Check that module, version, language are valid 2. Check Cache: Look for cached compilation result (L1 → L2) 3. Prepare Proto Files: Write proto files to temporary directory 4. Resolve Dependencies: Fetch and prepare dependency proto files 5. Create Docker Container: Launch container with protoc + language plugin 6. Execute protoc: Run protoc with appropriate flags inside container 7. Generate Package Files: Create go.mod, setup.py, package.json, etc. 8. Collect Artifacts: Extract generated files from container 9. Cache Result: Store in L1 and L2 cache 10. Upload to S3: Optionally upload tarball to S3 11. Return Result: Return generated files to caller

Caching Strategy

The orchestrator uses two-tier caching for performance:

L1 (In-Memory): Fast access, limited size (default 10MB), short TTL (5 minutes). Best for repeated compilations within the same server instance.

// L1 cache hit: ~1ms
result, _ := orch.CompileSingle(ctx, req) // First call compiles
result, _ := orch.CompileSingle(ctx, req) // Second call uses L1

L2 (Redis): Shared across server instances, larger capacity, longer TTL (24 hours). Best for popular modules that multiple servers compile frequently.

// L2 cache hit: ~50ms (Redis network latency)
// Server A compiles module
resultA, _ := orchA.CompileSingle(ctx, req)
// Server B gets cached result from Redis
resultB, _ := orchB.CompileSingle(ctx, req) // Uses L2

Cache keys are generated from:

  • Module name and version
  • Language and plugin version
  • Combined hash of all proto files (including dependencies)
  • Compilation options

This ensures cache hits only when truly identical compilation would occur.

Dependency Management

Proto files often import other proto files:

// user.proto
import "common/types.proto";

message User {
	common.UUID id = 1;
}

The orchestrator automatically resolves dependencies:

req := &orchestrator.CompileRequest{
	ModuleName: "user-service",
	Version:    "v1.0.0",
	Language:   "go",
	ProtoFiles: []codegen.ProtoFile{
		{Path: "user.proto", Content: userProtoContent},
	},
	Dependencies: []codegen.Dependency{
		{
			ModuleName: "common",
			Version:    "v1.2.0",
			ProtoFiles: []codegen.ProtoFile{
				{Path: "common/types.proto", Content: typesProtoContent},
			},
		},
	},
}

The orchestrator:

  1. Creates proto_path with module + all dependencies
  2. Runs protoc with --proto_path pointing to all directories
  3. Generates code with proper import paths

Docker-Based Compilation

V2 uses Docker for isolation and consistency:

Docker Image: spoke-protoc:<language>-<version>
Example: spoke-protoc:go-1.32.0

Each image contains:

  • protoc (Protocol Buffers compiler)
  • Language-specific plugin (protoc-gen-go, protoc-gen-python, etc.)
  • Runtime dependencies (go compiler, python interpreter, etc.)

Container workflow:

  1. Create container from language-specific image
  2. Mount proto files as volume: /workspace
  3. Execute: protoc --<lang>_out=/output --proto_path=/workspace ...
  4. Extract generated files from /output
  5. Clean up container

Benefits:

  • Consistent environment (no "works on my machine")
  • Isolated from host (security and reproducibility)
  • Pin specific plugin versions (go plugin 1.32.0 always available)
  • No local installation required (just Docker)

Package Manager File Generation

The system generates package manager files for each language:

Go (go.mod):

module github.com/example/user-service
go 1.21

require (
	google.golang.org/protobuf v1.32.0
	google.golang.org/grpc v1.60.0
)

Python (setup.py):

from setuptools import setup, find_packages

setup(
	name="user-service",
	version="1.0.0",
	packages=find_packages(),
	install_requires=[
		"protobuf>=4.25.0",
		"grpcio>=1.60.0",
	],
)

TypeScript (package.json):

{
	"name": "@example/user-service",
	"version": "1.0.0",
	"dependencies": {
		"google-protobuf": "^3.21.0",
		"@grpc/grpc-js": "^1.9.0"
	}
}

Package generators are registered in pkg/codegen/packages/registry.go.

Artifact Storage

Compiled artifacts can be stored in S3 for long-term retention:

config := orchestrator.DefaultConfig()
config.S3Bucket = "spoke-artifacts"
config.S3Prefix = "compiled/"
config.S3Region = "us-east-1"
orch, _ := orchestrator.NewOrchestrator(config)

result, _ := orch.CompileSingle(ctx, req)
// Automatically uploaded to: s3://spoke-artifacts/compiled/user-service/v1.0.0/go.tar.gz

Artifacts are stored as compressed tarballs with checksums:

user-service-v1.0.0-go.tar.gz
user-service-v1.0.0-go.tar.gz.sha256

Clients can download pre-compiled artifacts instead of compiling locally.

Error Handling

Compilation can fail for various reasons:

result, err := orch.CompileSingle(ctx, req)
if err != nil {
	// Orchestrator-level error (Docker unavailable, etc.)
	log.Fatal(err)
}

if !result.Success {
	// Compilation error (proto syntax error, missing import, etc.)
	fmt.Printf("Compilation failed: %s\n", result.Error)
}

Common error types:

ErrLanguageNotSupported  - Language not registered
ErrDockerNotAvailable    - Docker daemon not running
ErrProtoSyntaxError      - Invalid proto syntax
ErrMissingImport         - Import not found in dependencies
ErrPluginCrash           - protoc plugin crashed
ErrTimeout               - Compilation exceeded timeout

Configuration

Orchestrator configuration options:

config := &orchestrator.Config{
	// Parallelism
	MaxParallelWorkers: 10,  // Compile 10 languages simultaneously

	// Caching
	EnableCache:    true,
	RedisAddr:      "redis:6379",
	RedisPassword:  "secret",

	// Storage
	S3Bucket:       "spoke-artifacts",
	S3Prefix:       "compiled/",
	S3Region:       "us-east-1",

	// Timeouts
	CompilationTimeout: 300,  // 5 minutes max per compilation
}

Performance Considerations

Compilation can be expensive. Optimize by:

1. Enable caching to avoid redundant compilation:

config.EnableCache = true
config.RedisAddr = "redis:6379"  // Share cache across servers

2. Pre-compile during version push rather than on-demand:

// In version creation handler
languages := []string{"go", "python", "java"}
results, _ := orch.CompileAll(ctx, req, languages)
// Store results in database

3. Use parallel compilation for multiple languages:

// Compiles all 3 languages simultaneously (if workers available)
results, _ := orch.CompileAll(ctx, req, []string{"go", "python", "java"})

4. Store artifacts in S3 and serve pre-compiled downloads:

// Client downloads tarball instead of compiling
GET /modules/user-service/versions/v1.0.0/download/go
→ Returns: s3://spoke-artifacts/compiled/user-service/v1.0.0/go.tar.gz

5. Monitor cache hit rates and adjust TTL:

metrics := orch.GetMetrics()
fmt.Printf("Cache hit rate: %.1f%%\n", metrics.CacheHitRate*100)

Testing

Test compilation without Docker:

// Mock the Docker runner
type mockRunner struct {
	docker.Runner
	executeFunc func(*docker.ExecutionRequest) (*docker.ExecutionResult, error)
}

// In test
mock := &mockRunner{
	executeFunc: func(req *docker.ExecutionRequest) (*docker.ExecutionResult, error) {
		return &docker.ExecutionResult{
			Success: true,
			Files: []docker.OutputFile{
				{Path: "user.pb.go", Content: []byte("package user")},
			},
		}, nil
	},
}

For integration tests, use real Docker:

func TestCompilation(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping Docker-based test")
	}

	orch, _ := orchestrator.NewOrchestrator(nil)
	result, err := orch.CompileSingle(ctx, req)
	assert.NoError(t, err)
	assert.True(t, result.Success)
}

V1 vs V2 Migration

The v1 backend is simpler but less capable. The v2 backend (orchestrator) adds:

Feature                  V1      V2
--------                 --      --
Basic compilation        Yes     Yes
Dependency resolution    No      Yes
Caching                  No      Yes
S3 artifact storage      No      Yes
Package file generation  No      Yes
Parallel compilation     No      Yes
Plugin version pinning   No      Yes
Docker isolation         No      Yes

The orchestrator-based compilation system is the only supported compilation method. Legacy v1 (direct protoc invocation) has been removed.

Subpackages

  • orchestrator: Compilation workflow coordination
  • languages: Language specs and plugin registry
  • docker: Container-based protoc execution
  • cache: Two-tier caching (L1 in-memory, L2 Redis)
  • packages: Package manager file generation
  • artifacts: S3 storage for compiled artifacts
  • pkg/api: HTTP API that invokes compilation
  • pkg/storage: Stores proto files and compiled artifacts
  • pkg/dependencies: Resolves proto import relationships
  • pkg/validation: Validates proto files before compilation
  • pkg/compatibility: Checks compatibility between versions

Design Decisions

Docker-Based Execution: Using Docker ensures consistent, reproducible builds across environments. No more "it compiles on my laptop but not in CI" issues.

Two-Tier Caching: L1 for speed (in-memory), L2 for sharing (Redis). This balances performance with resource efficiency - hot modules use L1, warm modules use L2.

Async Compilation: Large compilations can be queued and processed asynchronously, preventing API timeouts. Clients poll for status and retrieve results when ready.

Language Registry: Centralizing language specifications makes it easy to add new languages or update plugin versions without touching orchestrator code.

Package Generation: Generating go.mod, setup.py, etc. makes compiled artifacts immediately usable - users can directly import without manual setup.

Content-Addressed Caching: Cache keys include proto file hashes, ensuring cache hits only occur when truly identical compilation would happen. Prevents serving stale artifacts.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClearCache

func ClearCache()

ClearCache clears the global memory cache

func RegisterLanguage

func RegisterLanguage(spec *LanguageSpec)

RegisterLanguage registers a language specification

func RegisterPackageGenerator

func RegisterPackageGenerator(name string, gen PackageGenerator)

RegisterPackageGenerator registers a package generator

Types

type CacheKey

type CacheKey struct {
	ModuleName    string
	Version       string
	Language      string
	PluginVersion string
	ProtoHash     string            // Combined hash of all proto files + dependencies (generated with sorted inputs)
	Options       map[string]string // Plugin options (hashed with sorted keys)
}

CacheKey represents a key for caching compiled artifacts CacheKey represents a unique identifier for cached compilation results

CRITICAL INVARIANT: Cache keys must be generated with sorted inputs. Use cache.GenerateCacheKey() to create keys - never construct manually.

Cache Key Format Version: v1 String format: {moduleName}:{version}:{language}:{pluginVersion}:{protoHash}:{optionsHash}

WARNING: Changing the String() method or key generation algorithm INVALIDATES ALL CACHED COMPILATIONS system-wide.

If you must modify: 1. Increment cache format version number 2. Clear all cached data or implement migration 3. Update cache package documentation 4. Test that identical inputs produce identical keys

func (*CacheKey) String

func (k *CacheKey) String() string

String returns the cache key as a string

type CompilationJob

type CompilationJob struct {
	ID          string
	VersionID   int64
	Language    string
	Status      JobStatus
	StartedAt   *time.Time
	CompletedAt *time.Time
	Error       string
	CacheHit    bool
	Result      *CompilationResult
}

CompilationJob represents an async compilation job

type CompilationMetrics

type CompilationMetrics struct {
	Language      string
	Duration      time.Duration
	CacheHit      bool
	GeneratedSize int64
	Success       bool
}

CompilationMetrics tracks compilation performance

type CompilationRequest

type CompilationRequest struct {
	// Module information
	ModuleName string
	Version    string
	VersionID  int64

	// Proto files and dependencies
	ProtoFiles   []ProtoFile
	Dependencies []Dependency

	// Language and options
	Language    string
	IncludeGRPC bool
	Options     map[string]string

	// Context
	Context context.Context
}

CompilationRequest represents a request to compile proto files for a language

type CompilationResult

type CompilationResult struct {
	Success        bool
	Language       string
	GeneratedFiles []GeneratedFile
	PackageFiles   []GeneratedFile // go.mod, setup.py, package.json, etc.
	CacheHit       bool
	Duration       time.Duration
	Error          string

	// Storage information
	S3Key        string
	S3Bucket     string
	ArtifactHash string
}

CompilationResult represents the result of a compilation

func GenerateCode deprecated

func GenerateCode(ctx context.Context, req *GenerateRequest, config *Config) (*CompilationResult, error)

GenerateCode is a backward-compatible wrapper for legacy callers

Deprecated: Use Generator.GenerateCode() with dependency injection instead. This function uses a global generator instance which makes testing difficult.

Migration:

// Old (global state):
result, err := codegen.GenerateCode(ctx, req, config)

// New (dependency injection):
generator := codegen.NewGenerator()
result, err := generator.GenerateCode(ctx, req, config)

func GenerateCodeParallel deprecated

func GenerateCodeParallel(ctx context.Context, req *GenerateRequest, languages []string, config *Config) ([]*CompilationResult, error)

GenerateCodeParallel is a backward-compatible wrapper for legacy callers

Deprecated: Use Generator.GenerateCodeParallel() with dependency injection instead. This function uses a global generator instance which makes testing difficult.

Migration:

// Old (global state):
results, err := codegen.GenerateCodeParallel(ctx, req, langs, config)

// New (dependency injection):
generator := codegen.NewGenerator()
results, err := generator.GenerateCodeParallel(ctx, req, langs, config)

type Config

type Config struct {
	MaxWorkers  int
	Timeout     time.Duration
	EnableCache bool
}

Config holds code generation configuration

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns default configuration

type Dependency

type Dependency struct {
	ModuleName string
	Version    string
	ProtoFiles []ProtoFile
}

Dependency represents a module dependency

type DockerRequest

type DockerRequest struct {
	Image       string
	Tag         string
	ProtoFiles  []ProtoFile
	ProtocFlags []string
	Timeout     time.Duration
}

DockerRequest represents a Docker execution request

type DockerResult

type DockerResult struct {
	GeneratedFiles []GeneratedFile
	Duration       time.Duration
	ExitCode       int
	Stdout         string
	Stderr         string
}

DockerResult represents the result of Docker execution

func ExecuteDocker

func ExecuteDocker(ctx context.Context, req *DockerRequest) (*DockerResult, error)

ExecuteDocker runs protoc in a Docker container using os/exec

type GenerateRequest

type GenerateRequest struct {
	ModuleName   string
	Version      string
	ProtoFiles   []ProtoFile
	Dependencies []Dependency
	Language     string
	IncludeGRPC  bool
	Options      map[string]string
}

GenerateRequest represents a code generation request

type GeneratedFile

type GeneratedFile struct {
	Path    string // Relative path within output directory
	Content []byte
	Size    int64
}

GeneratedFile represents a single generated file

type Generator

type Generator struct {
	// contains filtered or unexported fields
}

Generator is a code generator with dependency injection for cache

IMPORTANT: Use NewGenerator() to create instances. The zero value is not usable.

Benefits of dependency injection over global cache:

  • Testable: Each test can have isolated cache state
  • Configurable: Different cache implementations can be injected
  • Clear dependencies: Cache dependency is explicit in struct
  • Thread-safe: Multiple generators can coexist without interference

func NewGenerator

func NewGenerator() *Generator

NewGenerator creates a new code generator with injected dependencies

func (*Generator) GenerateCode

func (g *Generator) GenerateCode(ctx context.Context, req *GenerateRequest, config *Config) (*CompilationResult, error)

GenerateCode compiles proto files for a single language using the generator's cache

func (*Generator) GenerateCodeParallel

func (g *Generator) GenerateCodeParallel(ctx context.Context, req *GenerateRequest, languages []string, config *Config) ([]*CompilationResult, error)

GenerateCodeParallel compiles proto files for multiple languages in parallel

type JobStatus

type JobStatus string

JobStatus represents the status of a compilation job

const (
	JobStatusPending   JobStatus = "pending"
	JobStatusRunning   JobStatus = "running"
	JobStatusCompleted JobStatus = "completed"
	JobStatusFailed    JobStatus = "failed"
)

type LanguageSpec

type LanguageSpec struct {
	ID             string
	Name           string
	Enabled        bool
	DockerImage    string
	DockerTag      string
	ProtocFlags    []string
	SupportsGRPC   bool
	GRPCFlags      []string
	PluginVersion  string
	PackageManager *PackageManagerSpec
}

LanguageSpec defines a language configuration

func GetLanguageSpec

func GetLanguageSpec(langID string) (*LanguageSpec, error)

GetLanguageSpec retrieves a language specification

type PackageGenerator

type PackageGenerator interface {
	Generate(req *PackageRequest) ([]GeneratedFile, error)
}

PackageGenerator generates package manager files

func GetPackageGenerator

func GetPackageGenerator(name string) PackageGenerator

GetPackageGenerator retrieves a package generator

type PackageManagerSpec

type PackageManagerSpec struct {
	Name    string
	Enabled bool
}

PackageManagerSpec defines package manager configuration

type PackageRequest

type PackageRequest struct {
	ModuleName  string
	Version     string
	Language    string
	IncludeGRPC bool
	Options     map[string]string
}

PackageRequest represents a package file generation request

type ProtoFile

type ProtoFile struct {
	Path    string // Relative path within the module
	Content []byte
	Hash    string // SHA256 hash for cache key generation
}

ProtoFile represents a single proto file

Directories

Path Synopsis
Package cache provides cache key generation and formatting for compilation results
Package cache provides cache key generation and formatting for compilation results
Package config provides default configuration values for the codegen system
Package config provides default configuration values for the codegen system
npm
pip

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL