Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,43 @@

All notable changes to this project will be documented in this file.

## [v2.2.0] - 2026-04-13

### Features

- **Concurrent Request Handling**: A single container instance can now handle multiple concurrent requests (up to a configurable limit), improving resource utilization and drastically reducing cold starts by 42%.
- **Smart Pooling Logic**: Updated the pool manager to keep containers in the "Idle" pool until they reach their concurrency threshold, allowing for better "saturation" routing.
- **Monitoring Improvements**: Added `ColdStart` field to invocation responses to track and benchmark container creation events.

### Performance

- **Cold Start Latency**: Reduced to **~340 ms** (from ~590 ms).
- **Warm Throughput**: Increased to **~1,900 req/s** (from ~1,100 req/s) due to concurrent execution.
- **Warm Start Latency**: Stable at **~1.3 ms**.

### Documentation & Bug Fixes

- Updated README with latest benchmark results and design choices referencing IEEE methodologies.
- Fixed several race conditions in the container pool acquisition logic.
- Ensured `MaxConcurrency` settings are safely handled as `int32`.

---

## [v2.1.0] - 2026-04-12

### Features

- **Per-Function Rate Limiting**: Added support for setting a maximum requests per second limit during function deployment.
- **Dynamic Configuration**: New `POST /config/:name` endpoint allowing real-time updates to function rate limits without redeploying.
- **Intelligent Burst Scaling**: Implemented a burst logic that scales at 10% of the rate limit to provide better throughput management.

### Performance

- **Warm Throughput**: Increased to **~1,100 req/s**.
- **Warm Start Latency**: Improved to **~1.3 ms**.

### Documentation & Tests

- Updated README with new API documentation and latest benchmark results.
- Added comprehensive unit and integration tests for rate limiting and configuration management.

Expand All @@ -22,6 +47,7 @@ All notable changes to this project will be documented in this file.
## [v2.0.0] - 2026-04-11

### Features

- **Persistent Workers**: Transitioned to persistent Unix Domain Socket (UDS) servers for workers, significantly improving invocation performance.
- **Auto-Scaling & Latency**: Achieved a **99.6% reduction in latency** through auto-scaling container pools and persistent UDS workers.
- **Log Management**: Functions now support log extraction and storage in a centralized SQLite database.
Expand All @@ -32,13 +58,15 @@ All notable changes to this project will be documented in this file.
- **Directory Structure**: Simplified storage by using a `.glambdar` directory for worker scripts and deployed functions.

### Refactoring & Improvements

- **Docker SDK Migration**: Transitioned from using Docker CLI via `exec.Command` to the official Docker SDK for more robust container management.
- Centralized configuration management in `internal/config`.
- Reorganized project structure by moving the entry point to the root.
- Added comprehensive unit tests for logging functionality.
- Exported `BaseDir` path for better internal visibility.

### Bug Fixes

- Fixed Docker client lifecycle management to ensure closure after server shutdown.
- Ensured `functions/` directory is created if it does not exist.
- Improved error messaging across the system.
Expand All @@ -47,4 +75,5 @@ All notable changes to this project will be documented in this file.
---

## [v1.0.0] - 2026-04-10

Initial release with support for basic function deployment and invocation.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ Glambdar is optimized for low-latency function execution using persistent per-fu

| Metric | Result |
| ---------------------------- | ---------------- |
| **Cold Start Latency** | **~590 ms** |
| **Cold Start Latency** | **~340 ms** |
| **Warm Start Latency (Avg)** | **~1.3 ms** |
| **Warm Throughput** | **~1,100 req/s** |
| **Warm Throughput** | **~1,900 req/s** |

**Benchmark Environment**

Expand All @@ -290,6 +290,7 @@ Glambdar is optimized for low-latency function execution using persistent per-fu
## Design choices

- **Persistent Docker container pool** for reduced latency and auto-scaling
- **Intra-Function Concurrency:** Implemented a multi-request routing threshold (adapted from 2024 IEEE serverless optimization models) to drastically reduce cold starts under burst loads while maintaining strict process isolation.
- **UDS over TCP** for low-latency IPC
- Simple IPC protocol (structured JSON)

Expand Down
2 changes: 1 addition & 1 deletion internal/api/invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestInvokeHandler_RateLimited(t *testing.T) {
})

// Mock a rate-limited pool
p := config.PoolManager.GetOrCreate(funcName, 1)
p := config.PoolManager.GetOrCreate(funcName, 1, 1)
p.Limiter = rate.NewLimiter(rate.Limit(0), 0) // Never allow

router := Router()
Expand Down
9 changes: 5 additions & 4 deletions internal/functions/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ func Deploy(zipFilePath string, funcName string, rateLimit int) error {

// Initialize function metadata
meta := Metadata{
Name: funcName,
CreatedAt: time.Now().UTC(),
InvokeCount: 0,
RateLimit: rateLimit,
Name: funcName,
CreatedAt: time.Now().UTC(),
InvokeCount: 0,
RateLimit: rateLimit,
MaxConcurrency: 10,
}
if err := SaveMetadata(&meta); err != nil {
return err
Expand Down
34 changes: 21 additions & 13 deletions internal/functions/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/eswar-7116/glambdar/internal/config"
"github.com/eswar-7116/glambdar/internal/docker"
"github.com/eswar-7116/glambdar/internal/pool"
"github.com/moby/moby/api/pkg/stdcopy"
)

Expand All @@ -28,6 +29,7 @@ type InvokeResponse struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body json.RawMessage `json:"body"`
ColdStart bool `json:"coldStart"`
}

func Invoke(ctx context.Context, d *docker.Docker, funcName string, req InvokeRequest) (InvokeResponse, error) {
Expand All @@ -52,47 +54,52 @@ func Invoke(ctx context.Context, d *docker.Docker, funcName string, req InvokeRe
}

// Acquire a warm container or create a new one
p := config.PoolManager.GetOrCreate(funcName, md.RateLimit)
p := config.PoolManager.GetOrCreate(funcName, md.RateLimit, md.MaxConcurrency)
if !p.Limiter.Allow() {
return InvokeResponse{}, ErrRateLimited
}

containerID, socketPath, warm := p.Acquire()
e, warm := p.Acquire()
if !warm {
// Generate a per-container socket directory on the host
socketDir, err := os.MkdirTemp("", "glambdar-sock-*")
if err != nil {
return InvokeResponse{}, fmt.Errorf("failed to create socket dir: %w", err)
}
os.Chmod(socketDir, 0777)
socketPath = socketDir

containerID, err = d.ContainerCreate(ctx, funcDir, socketPath)
containerID, err := d.ContainerCreate(ctx, funcDir, socketDir)
if err != nil {
os.RemoveAll(socketPath)
os.RemoveAll(socketDir)
return InvokeResponse{}, fmt.Errorf("failed to create container: %w", err)
}

// Start the container
if err := d.ContainerStart(ctx, containerID); err != nil {
os.RemoveAll(socketPath)
os.RemoveAll(socketDir)
return InvokeResponse{}, fmt.Errorf("failed to start container: %w", err)
}

// Wait for the worker's UDS server to become ready
workerSock := filepath.Join(socketPath, "glambdar.sock")
workerSock := filepath.Join(socketDir, "glambdar.sock")
if err := waitForSocket(workerSock, 5*time.Second); err != nil {
d.ContainerKill(ctx, containerID)
os.RemoveAll(socketPath)
os.RemoveAll(socketDir)
return InvokeResponse{}, fmt.Errorf("worker socket not ready: %w", err)
}

e = &pool.Entry{
ContainerID: containerID,
SocketPath: socketDir,
ActiveRequests: 1, // this current request
}
}

// Release container back to pool (or kill if pool is full) on return
defer func() {
if !p.Release(containerID, socketPath) {
d.ContainerKill(ctx, containerID)
os.RemoveAll(socketPath)
if !p.Release(e) {
d.ContainerKill(ctx, e.ContainerID)
os.RemoveAll(e.SocketPath)
}
}()

Expand All @@ -103,7 +110,7 @@ func Invoke(ctx context.Context, d *docker.Docker, funcName string, req InvokeRe
}

// Dial the container's UDS and send the request
workerSock := filepath.Join(socketPath, "glambdar.sock")
workerSock := filepath.Join(e.SocketPath, "glambdar.sock")
conn, err := net.DialTimeout("unix", workerSock, 5*time.Second)
if err != nil {
return InvokeResponse{}, fmt.Errorf("failed to dial worker socket: %w", err)
Expand All @@ -127,7 +134,7 @@ func Invoke(ctx context.Context, d *docker.Docker, funcName string, req InvokeRe
}

// Process logs
out, err := d.ContainerLogs(ctx, containerID, md.LastInvokedAt.Format(time.RFC3339))
out, err := d.ContainerLogs(ctx, e.ContainerID, md.LastInvokedAt.Format(time.RFC3339))
if err == nil {
defer out.Close()
var stdout, stderr bytes.Buffer
Expand All @@ -142,6 +149,7 @@ func Invoke(ctx context.Context, d *docker.Docker, funcName string, req InvokeRe
})
}

res.ColdStart = !warm
return res, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/functions/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Metadata struct {
LastInvokedAt time.Time `json:"lastInvokedAt"`
InvokeCount int `json:"invokeCount"`
RateLimit int `json:"rateLimit" gorm:"default:0"` // 0 = unlimited
MaxConcurrency int32 `json:"maxConcurrency" gorm:"default:10"`
}

func LoadMetadata(funcName string) (*Metadata, error) {
Expand Down
27 changes: 14 additions & 13 deletions internal/pool/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ type PoolManager struct {
pools sync.Map // funcName -> *ContainerPool
}

func (pm *PoolManager) GetOrCreate(funcName string, rateLimit int) *ContainerPool {
func (pm *PoolManager) GetOrCreate(funcName string, rateLimit int, maxConcurrency int32) *ContainerPool {
p, _ := pm.pools.LoadOrStore(funcName, &ContainerPool{
idle: make(chan entry, 10),
Limiter: newLimiter(rateLimit),
Idle: make(chan *Entry, 10),
Limiter: newLimiter(rateLimit),
MaxConcurrency: maxConcurrency,
})
return p.(*ContainerPool)
}
Expand Down Expand Up @@ -57,12 +58,12 @@ func (pm *PoolManager) DeleteAllContainers(ctx context.Context, d *docker.Docker
p := val.(*ContainerPool)
for {
select {
case e := <-p.idle:
err := d.ContainerRemove(ctx, e.containerID)
case e := <-p.Idle:
err := d.ContainerRemove(ctx, e.ContainerID)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete container (%s): %s\n", e.containerID[:12], err)
fmt.Fprintf(os.Stderr, "Failed to delete container (%s): %s\n", e.ContainerID[:12], err)
}
os.RemoveAll(e.socketPath)
os.RemoveAll(e.SocketPath)
default:
return true // pool drained, move to next
}
Expand All @@ -75,15 +76,15 @@ func (pm *PoolManager) RemoveStaleContainers(ctx context.Context, d *docker.Dock
p := val.(*ContainerPool)
for {
select {
case e := <-p.idle:
if time.Since(e.lastUsed) > ttl {
err := d.ContainerRemove(ctx, e.containerID)
case e := <-p.Idle:
if time.Since(e.LastUsed) > ttl {
err := d.ContainerRemove(ctx, e.ContainerID)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to remove stale container (%s): %s\n", e.containerID[:12], err)
fmt.Fprintf(os.Stderr, "Failed to remove stale container (%s): %s\n", e.ContainerID[:12], err)
}
os.RemoveAll(e.socketPath)
os.RemoveAll(e.SocketPath)
} else {
p.idle <- e
p.Idle <- e
return true
}
default:
Expand Down
10 changes: 5 additions & 5 deletions internal/pool/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import (
func TestPoolManager_GetOrCreate(t *testing.T) {
pm := &PoolManager{}

p1 := pm.GetOrCreate("func-1", 0)
p1 := pm.GetOrCreate("func-1", 0, 1)
if p1 == nil {
t.Fatalf("Expected pool for func-1, got nil")
}

// Should return the same pool instance
p2 := pm.GetOrCreate("func-1", 0)
p2 := pm.GetOrCreate("func-1", 0, 1)
if p1 != p2 {
t.Errorf("Expected same pool instance for same funcName, got different")
}

// Should return a different pool instance for a different func
p3 := pm.GetOrCreate("func-2", 0)
p3 := pm.GetOrCreate("func-2", 0, 1)
if p1 == p3 {
t.Errorf("Expected different pool instance, got same")
}
Expand All @@ -29,7 +29,7 @@ func TestPoolManager_GetOrCreate(t *testing.T) {
pm.Delete("func-1")

// Should create a new one now since the old was deleted
p4 := pm.GetOrCreate("func-1", 0)
p4 := pm.GetOrCreate("func-1", 0, 1)
if p1 == p4 {
t.Errorf("Expected new pool instance after delete, got same")
}
Expand All @@ -40,7 +40,7 @@ func TestPoolManager_UpdateLimiter(t *testing.T) {
funcName := "update-limit-func"

// Initial create with limit 10
p := pm.GetOrCreate(funcName, 10)
p := pm.GetOrCreate(funcName, 10, 1)
if p.Limiter.Limit() != 10 {
t.Errorf("expected limit 10, got %v", p.Limiter.Limit())
}
Expand Down
Loading
Loading