Skip to content
Open
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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.git
.gitignore
.node_modules
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for any .node_modules directory in the repository
fd -HI -t d '^\.node_modules$' .

Repository: leynos/wildside

Length of output: 41


Fix the .dockerignore dependency ignore pattern.

  • Replace or remove the .node_modules entry on .dockerignore line 3: it only matches a literal root folder named .node_modules (none exists), while **/node_modules on line 8 already ignores the standard node_modules directory everywhere in the build context.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.dockerignore at line 3, Remove or replace the incorrect literal
`.node_modules` entry in .dockerignore: delete the `.node_modules` line (the
literal name matches nothing) since `**/node_modules` already covers standard
node_modules folders, or if you intended to ignore a hidden folder specifically,
change it to an explicit pattern that matches that target; update the
`.node_modules` entry accordingly so only the correct node_modules patterns
remain.

.tmp
.uv-cache
**/.terraform
**/.uv-cache
**/node_modules
**/target
coverage
frontend-pwa/dist
target
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ MARKDOWNLINT_CLI2_VERSION ?= 0.14.0
YAMLLINT_VERSION ?= 1.35.1
OPENAPI_SPEC ?= spec/openapi.json

# Place one consolidated PHONY declaration near the top of the file
.PHONY: all clean be fe fe-build openapi gen docker-up docker-down fmt lint test test-rust test-frontend typecheck deps lockfile lint-specs \
check-fmt markdownlint markdownlint-docs mermaid-lint nixie yamllint audit \
lint-rust lint-frontend lint-asyncapi lint-openapi lint-makefile lint-actions \
lint-architecture workspace-sync
.PHONY: all clean be fe fe-build openapi gen docker-up docker-down
.PHONY: local-k8s-up local-k8s-down local-k8s-status local-k8s-logs
.PHONY: fmt lint test test-rust test-frontend typecheck deps lockfile lint-specs
.PHONY: check-fmt markdownlint markdownlint-docs mermaid-lint nixie yamllint audit
.PHONY: lint-rust lint-frontend lint-asyncapi lint-openapi lint-makefile lint-actions
.PHONY: lint-architecture workspace-sync

workspace-sync:
./scripts/sync_workspace_members.py
Expand Down Expand Up @@ -78,6 +79,18 @@ docker-up:
docker-down:
cd deploy && docker compose down

local-k8s-up:
uv run scripts/local_k8s.py up

local-k8s-down:
uv run scripts/local_k8s.py down

local-k8s-status:
uv run scripts/local_k8s.py status

local-k8s-logs:
uv run scripts/local_k8s.py logs

fmt: workspace-sync
cargo fmt --all
$(call exec_or_bunx,biome,format --write frontend-pwa packages,@biomejs/biome@$(BIOME_VERSION))
Expand Down
214 changes: 214 additions & 0 deletions backend/src/domain/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//! Domain health observations for process liveness and readiness.
//!
//! The health model is intentionally small: it records whether the process
//! should be considered alive and whether it is ready to receive traffic. HTTP,
//! Kubernetes, Docker, and Helm adapters map these domain observations to their
//! own protocols.

use std::sync::atomic::{AtomicBool, Ordering};

use crate::domain::ports::HealthObserver;

/// Health status reported by a domain health observation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HealthStatus {
/// The observed capability is available.
Healthy,
/// The observed capability is unavailable.
Unhealthy,
}

impl HealthStatus {
/// Return whether this status represents a healthy observation.
///
/// # Examples
///
/// ```
/// use backend::domain::HealthStatus;
///
/// assert!(HealthStatus::Healthy.is_healthy());
/// assert!(!HealthStatus::Unhealthy.is_healthy());
/// ```
pub fn is_healthy(self) -> bool {
matches!(self, Self::Healthy)
}
}

/// A liveness or readiness observation owned by the domain layer.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HealthObservation {
status: HealthStatus,
}

impl HealthObservation {
/// Build a healthy observation.
///
/// # Examples
///
/// ```
/// use backend::domain::HealthObservation;
///
/// assert!(HealthObservation::healthy().is_healthy());
/// ```
pub fn healthy() -> Self {
Self {
status: HealthStatus::Healthy,
}
}

/// Build an unhealthy observation.
///
/// # Examples
///
/// ```
/// use backend::domain::HealthObservation;
///
/// assert!(!HealthObservation::unhealthy().is_healthy());
/// ```
pub fn unhealthy() -> Self {
Self {
status: HealthStatus::Unhealthy,
}
}

/// Return this observation's status.
pub fn status(self) -> HealthStatus {
self.status
}

/// Return whether this observation is healthy.
pub fn is_healthy(self) -> bool {
self.status.is_healthy()
}
}

/// Shared process health state used by runtime adapters.
///
/// New instances start live but not ready. The server composition root marks
/// readiness once the HTTP listener has been constructed.
pub struct ProcessHealth {
ready: AtomicBool,
live: AtomicBool,
}

impl Default for ProcessHealth {
fn default() -> Self {
Self {
ready: AtomicBool::new(false),
live: AtomicBool::new(true),
}
}
}

impl ProcessHealth {
/// Create health state starting live but not ready.
///
/// # Examples
///
/// ```
/// use backend::domain::ProcessHealth;
/// use backend::domain::ports::HealthObserver;
///
/// let health = ProcessHealth::new();
/// assert!(health.observe_liveness().is_healthy());
/// assert!(!health.observe_readiness().is_healthy());
/// ```
pub fn new() -> Self {
Self::default()
}

/// Mark the process as ready to serve traffic.
pub fn mark_ready(&self) {
self.ready.store(true, Ordering::Release);
}

/// Mark the process as not ready to serve traffic.
pub fn mark_not_ready(&self) {
self.ready.store(false, Ordering::Release);
}

/// Mark the process unhealthy so liveness checks fail.
pub fn mark_unhealthy(&self) {
self.live.store(false, Ordering::Release);
}

fn observation_from(is_healthy: bool) -> HealthObservation {
if is_healthy {
HealthObservation::healthy()
} else {
HealthObservation::unhealthy()
}
}
}

impl HealthObserver for ProcessHealth {
fn observe_liveness(&self) -> HealthObservation {
Self::observation_from(self.live.load(Ordering::Acquire))
}

fn observe_readiness(&self) -> HealthObservation {
Self::observation_from(self.ready.load(Ordering::Acquire))
}
}

#[cfg(test)]
mod tests {
//! Tests for domain health observations and state transitions.

use super::{HealthObservation, HealthStatus, ProcessHealth};
use crate::domain::ports::HealthObserver;
use rstest::{fixture, rstest};

#[fixture]
fn health() -> ProcessHealth {
ProcessHealth::new()
}

#[rstest]
fn default_health_starts_live_but_not_ready(health: ProcessHealth) {
assert_eq!(
health.observe_liveness().status(),
HealthStatus::Healthy,
"process should start live"
);
assert_eq!(
health.observe_readiness().status(),
HealthStatus::Unhealthy,
"process should not start ready before runtime initialisation"
);
}

#[rstest]
fn marking_ready_makes_readiness_healthy(health: ProcessHealth) {
health.mark_ready();

assert!(health.observe_readiness().is_healthy());
}

#[rstest]
fn marking_not_ready_makes_readiness_unhealthy(health: ProcessHealth) {
health.mark_ready();
health.mark_not_ready();

assert!(!health.observe_readiness().is_healthy());
}

#[rstest]
fn marking_unhealthy_makes_liveness_unhealthy(health: ProcessHealth) {
health.mark_unhealthy();

assert!(!health.observe_liveness().is_healthy());
}

#[rstest]
#[case(HealthObservation::healthy(), HealthStatus::Healthy, true)]
#[case(HealthObservation::unhealthy(), HealthStatus::Unhealthy, false)]
fn observations_report_status_and_predicate(
#[case] observation: HealthObservation,
#[case] expected_status: HealthStatus,
#[case] expected_healthy: bool,
) {
assert_eq!(observation.status(), expected_status);
assert_eq!(observation.is_healthy(), expected_healthy);
}
}
2 changes: 2 additions & 0 deletions backend/src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub mod er_diagram;
pub mod error;
#[cfg(feature = "example-data")]
pub mod example_data;
pub mod health;
pub mod idempotency;
pub mod interest_theme;
pub mod localization;
Expand Down Expand Up @@ -111,6 +112,7 @@ pub use self::er_diagram::{
pub use self::error::{Error, ErrorCode, ErrorValidationError};
#[cfg(feature = "example-data")]
pub use self::example_data::{ExampleDataSeedOutcome, ExampleDataSeeder, ExampleDataSeedingError};
pub use self::health::{HealthObservation, HealthStatus, ProcessHealth};
pub use self::idempotency::{
IdempotencyConfig, IdempotencyKey, IdempotencyKeyValidationError, IdempotencyLookupQuery,
IdempotencyLookupResult, IdempotencyRecord, MutationType, ParseMutationTypeError, PayloadHash,
Expand Down
12 changes: 12 additions & 0 deletions backend/src/domain/ports/health_observer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Domain port for observing runtime health.

use crate::domain::HealthObservation;

/// Observes process health without leaking adapter protocols into the domain.
pub trait HealthObserver {
/// Report whether the process should be considered alive.
fn observe_liveness(&self) -> HealthObservation;

/// Report whether the process is ready to receive traffic.
fn observe_readiness(&self) -> HealthObservation;
}
2 changes: 2 additions & 0 deletions backend/src/domain/ports/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod enrichment_job_metrics;
mod enrichment_provenance_repository;
mod example_data_runs_repository;
mod example_data_seed_repository;
mod health_observer;
mod idempotency_metrics;
mod idempotency_repository;
mod login_service;
Expand Down Expand Up @@ -92,6 +93,7 @@ pub use example_data_seed_repository::{
ExampleDataSeedRepository, ExampleDataSeedRepositoryError, ExampleDataSeedRequest,
ExampleDataSeedUser,
};
pub use health_observer::HealthObserver;
pub use idempotency_metrics::{
IdempotencyMetricLabels, IdempotencyMetrics, IdempotencyMetricsError, NoOpIdempotencyMetrics,
};
Expand Down
Loading
Loading