Skip to content
Merged

Dev #20

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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,51 @@ Laminar can now route traffic using runtime-aware balancing strategies instead o

The current implementation intentionally keeps algorithm dispatch simple using enum matching and naive selection logic.
More advanced balancing abstractions and optimizations will evolve later as additional strategies are introduced.

---

## 2026-05-20

### Added

- Added retry stabilization to avoid retrying already-failed backends during a single request lifecycle
- Added runtime behavior tests for:
- connection guard lifecycle tracking
- timeout-based unhealthy backend handling
- retry stabilization behavior
- Added backend recovery logging for healthy state transitions
- Added Excalidraw runtime architecture diagram to README
- Added visual documentation for:
- runtime structs
- backend ownership model
- shared state relationships
- YAML configuration flow

### Changed

- Refactored proxy connection handling into isolated helper flow
- Improved timeout and proxy error logging with clearer runtime context
- Reduced health checker lock scope to avoid holding shared state locks across async network operations
- Improved retry logging with backend-aware runtime details
- Cleaned up retry flow readability and connection orchestration structure

### Notes

This phase focused heavily on runtime stability, observability, and internal architecture clarity.

Laminar now has stronger runtime behavior guarantees around:

- retry isolation
- backend failover handling
- timeout-aware connection management
- async-safe shared state access
- connection lifecycle tracking

The runtime architecture documentation was also expanded to better explain how:

- `AppState`
- `UpstreamPool`
- `BackendState`
- `ConnectionGuard`

interact during live traffic routing and health monitoring.
44 changes: 18 additions & 26 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@

- [x] Create listening socket
- [x] Bind address/port
- [ ] Configure socket options
- [ ] SO_REUSEADDR
- [ ] TCP_NODELAY
- [ ] Keepalive
- [x] Implement accept loop
- [x] Handle concurrent client connections
- [x] Handle client disconnects
- [ ] Implement connection cleanup
- [x] Implement connection cleanup

---

Expand All @@ -46,7 +42,7 @@

- [x] Implement backend registry
- [x] Track backend runtime state
- [ ] Add backend availability tracking
- [x] Add backend availability tracking
- [x] Implement backend selection interface

---
Expand Down Expand Up @@ -84,7 +80,7 @@

- [x] Create background health task
- [x] Add configurable health intervals
- [ ] Add backend recovery detection
- [x] Add backend recovery detection

---

Expand All @@ -107,10 +103,10 @@

## Retry Logic

- [ ] Retry failed backend connections
- [ ] Retry next available backend
- [ ] Add retry limits
- [ ] Add retry logging
- [x] Retry failed backend connections
- [x] Retry next available backend
- [x] Add retry limits
- [x] Add retry logging

---

Expand All @@ -125,20 +121,19 @@

## Runtime State Refactor

- [ ] Reduce lock scope sizes
- [ ] Refactor duplicated runtime logic
- [ ] Separate balancing module
- [ ] Separate health module
- [ ] Improve state ownership model
- [x] Reduce lock scope sizes
- [x] Separate balancing module
- [x] Separate health module
- [x] Improve state ownership model

---

## Phase 2 Deliverable

- [ ] Health-aware balancing
- [ ] Retry support
- [ ] Connection metrics
- [ ] Runtime stability improvements
- [x] Health-aware balancing
- [x] Retry support
- [x] Connection metrics
- [x] Runtime stability improvements

---

Expand Down Expand Up @@ -174,9 +169,9 @@

- [ ] Structured JSON logs
- [ ] Request correlation IDs
- [ ] Retry logging
- [ ] Timeout logging
- [ ] Backend transition logging
- [x] Retry logging
- [x] Timeout logging
- [x] Backend transition logging

---

Expand Down Expand Up @@ -219,9 +214,6 @@

## Event-Driven Runtime

- [ ] Integrate epoll
- [ ] Add edge-triggered events
- [ ] Implement event batching
- [ ] Add worker thread model

---
Expand Down
2 changes: 1 addition & 1 deletion tests/connect_timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async fn marks_backend_unhealthy_on_connect_timeout() {
let address = guard.address();
let result = timeout(Duration::from_millis(100), TcpStream::connect(address)).await;
assert!(result.is_err());

assert!(backend.healthy.load(Ordering::Relaxed));
guard.mark_backend_unhealthy();
assert!(!backend.healthy.load(Ordering::Relaxed));
}
32 changes: 32 additions & 0 deletions tests/connection_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
};

use laminar::{
config::types::BackendServerConfig,
state::backend::{BackendState, ConnectionGuard},
};

#[test]
fn connection_guard_tracks_active_connections() {
let backend = Arc::new(BackendState {
config: BackendServerConfig {
id: "server-1".to_string(),
host: "127.0.0.1".to_string(),
port: 9001,
weight: 1,
},
healthy: AtomicBool::new(true),
active_connections: AtomicUsize::new(0),
failed_health_checks: 0,
});

assert_eq!(backend.active_connections.load(Ordering::Relaxed), 0);
{
let _guard = ConnectionGuard::new(backend.clone());
assert_eq!(backend.active_connections.load(Ordering::Relaxed), 1);
}

assert_eq!(backend.active_connections.load(Ordering::Relaxed), 0);
}
43 changes: 43 additions & 0 deletions tests/retry_stabilization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
};

use laminar::{
algorithms::round_robin, config::types::BackendServerConfig, state::backend::BackendState,
};

#[test]
fn unhealthy_backend_is_not_selected_again() {
let backend_1 = Arc::new(BackendState {
config: BackendServerConfig {
id: "dead-server".to_string(),
host: "127.0.0.1".to_string(),
port: 9001,
weight: 1,
},

healthy: AtomicBool::new(false),
active_connections: AtomicUsize::new(0),
failed_health_checks: 0,
});

let backend_2 = Arc::new(BackendState {
config: BackendServerConfig {
id: "healthy-server".to_string(),
host: "127.0.0.1".to_string(),
port: 9002,
weight: 1,
},

healthy: AtomicBool::new(true),
active_connections: AtomicUsize::new(0),
failed_health_checks: 0,
});

let backends = vec![backend_1, backend_2.clone()];
let index = AtomicUsize::new(0);
let selected = round_robin::select_backend(&backends, &index).unwrap();
assert_eq!(selected.config.id, "healthy-server");
assert!(selected.healthy.load(Ordering::Relaxed));
}
Loading