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
1 change: 1 addition & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This directory contains architectural decision records for the Torrust Tracker D

| Status | Date | Decision | Summary |
| ----------- | ---------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| βœ… Accepted | 2025-11-07 | [ExecutionContext Wrapper Pattern](./execution-context-wrapper.md) | Use ExecutionContext wrapper around Container for future-proof command signatures |
| βœ… Accepted | 2025-11-03 | [Environment Variable Prefix](./environment-variable-prefix.md) | Use `TORRUST_TD_` prefix for all environment variables |
| βœ… Accepted | 2025-10-15 | [External Tool Adapters Organization](./external-tool-adapters-organization.md) | Consolidate external tool wrappers in `src/adapters/` for better discoverability |
| βœ… Accepted | 2025-10-10 | [Repository Rename to Deployer](./repository-rename-to-deployer.md) | Rename from "Torrust Tracker Deploy" to "Torrust Tracker Deployer" for production use |
Expand Down
231 changes: 231 additions & 0 deletions docs/decisions/execution-context-wrapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Decision: ExecutionContext Wrapper Pattern

## Status

Accepted

## Date

2025-11-07

## Context

During the implementation of the Dispatch Layer (Proposal 2 from the presentation layer reorganization epic #154), we needed to decide how command handlers should access application services. We had two main options:

1. **Direct Container Access**: Pass the `Container` directly to command handlers
2. **ExecutionContext Wrapper**: Create an `ExecutionContext` wrapper around the `Container`

### Current Architecture

The Dispatch Layer routes commands to handlers and needs to provide access to application services (user output, repositories, external tool clients, etc.). These services are managed by the dependency injection `Container`.

### Design Options Considered

#### Option 1: Direct Container Access

```rust
pub fn route_command(
command: Commands,
working_dir: &Path,
container: &Container,
) -> Result<(), CommandError>

// In handlers:
fn handle_create_command(container: &Container) {
let user_output = container.user_output();
// ...
}
```

#### Option 2: ExecutionContext Wrapper

```rust
pub struct ExecutionContext {
container: Arc<Container>,
}

pub fn route_command(
command: Commands,
working_dir: &Path,
context: &ExecutionContext,
) -> Result<(), CommandError>

// In handlers:
fn handle_create_command(context: &ExecutionContext) {
let user_output = context.user_output();
// ...
}
```

## Decision

We chose **Option 2: ExecutionContext Wrapper** for the following reasons:

### 1. Future-Proof Command Signatures

By introducing `ExecutionContext`, we can add execution-related data in the future without breaking existing command handler signatures:

```rust
pub struct ExecutionContext {
container: Arc<Container>,
// Future additions without breaking changes:
// request_id: RequestId,
// execution_metadata: ExecutionMetadata,
// tracing_context: TracingContext,
// user_permissions: UserPermissions,
// execution_timeout: Duration,
}
```

If we used `Container` directly, adding any execution context would require changing every command handler signature.

### 2. Clear Abstraction and Intent

`ExecutionContext` provides a logical abstraction for "everything a command needs to execute" rather than exposing the dependency injection container directly:

- **Container**: Implementation detail for dependency injection
- **ExecutionContext**: Execution abstraction for command handlers

This makes the intent clearer and separates concerns properly.

### 3. Type Safety and Interface Clarity

```rust
// Less clear: What is this container for? Bootstrapping? Testing? Execution?
fn handle_command(container: &Container)

// Clear: This is specifically for command execution
fn handle_command(context: &ExecutionContext)
```

### 4. Command-Specific Service Aggregation

ExecutionContext can provide command-specific convenience methods and service aggregations:

```rust
impl ExecutionContext {
// Direct service access
pub fn user_output(&self) -> &Arc<Mutex<UserOutput>> {
self.container.user_output()
}

// Future: Command-specific aggregated services
pub fn deployment_services(&self) -> DeploymentServices {
DeploymentServices {
provisioner: self.container.provisioner(),
configurator: self.container.configurator(),
validator: self.container.validator(),
}
}
}
```

### 5. Enhanced Testability

Different execution contexts can be created for different scenarios:

```rust
// Production context
let context = ExecutionContext::new(container);

// Test context with mocks
let context = TestExecutionContext::new(mock_container);

// Both can implement the same interface
trait ExecutionContextTrait {
fn user_output(&self) -> &Arc<Mutex<UserOutput>>;
}
```

### 6. Industry Pattern Alignment

Most frameworks use execution context patterns:

- **Spring Framework**: `ApplicationContext`
- **ASP.NET Core**: `HttpContext`
- **Express.js**: Request/Response context
- **Go**: `context.Context`

This aligns with established patterns for managing execution state.

## Consequences

### Positive

- **Future-Proof**: Can extend execution context without breaking command signatures
- **Clear Intent**: ExecutionContext clearly indicates its purpose for command execution
- **Better Abstraction**: Separates execution concerns from dependency injection mechanics
- **Enhanced Testability**: Enables different contexts for different testing scenarios
- **Industry Alignment**: Follows established patterns from major frameworks

### Negative

- **Initial Overhead**: Currently just a thin wrapper around Container
- **Additional Indirection**: One extra layer between commands and services
- **Learning Curve**: New developers need to understand the wrapper pattern

### Migration Path

If needed, migration from Container to ExecutionContext (or vice versa) is straightforward:

```rust
// From Container to ExecutionContext
fn handle_command(container: &Container) -> fn handle_command(context: &ExecutionContext)

// From ExecutionContext to Container
fn handle_command(context: &ExecutionContext) -> fn handle_command(container: &Container)
```

## Implementation Details

### Current Implementation

```rust
pub struct ExecutionContext {
container: Arc<Container>,
}

impl ExecutionContext {
pub fn new(container: Arc<Container>) -> Self {
Self { container }
}

pub fn container(&self) -> &Container {
&self.container
}

pub fn user_output(&self) -> &Arc<Mutex<UserOutput>> {
self.container.user_output()
}
}
```

### Usage Pattern

```rust
// In bootstrap/app.rs
let container = Arc::new(Container::new());
let context = ExecutionContext::new(container);

// In dispatch layer
route_command(command, working_dir, &context)?;

// In command handlers
fn handle_create_command(context: &ExecutionContext) {
let user_output = context.user_output();
// Command implementation
}
```

## Related Decisions

- [Presentation Layer Reorganization](../decisions/README.md) - Overall context for the four-layer architecture
- [Command State Return Pattern](./command-state-return-pattern.md) - How commands return typed states

## References

- [Epic #154: Presentation Layer Reorganization](https://github.com/torrust/torrust-tracker-deployer/issues/154)
- [Issue #156: Create Dispatch Layer](https://github.com/torrust/torrust-tracker-deployer/issues/156)
- [Spring Framework ApplicationContext](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction)
- [ASP.NET Core HttpContext](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context)
- [Go Context Package](https://pkg.go.dev/context)
1 change: 1 addition & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ cloudinit
Cockburn
concepsts
connrefused
configurator
containerd
cpus
creds
Expand Down
9 changes: 6 additions & 3 deletions src/bootstrap/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
//! - **Single Responsibility**: Focus only on application bootstrap concerns
//! - **Clean Separation**: No CLI parsing or business logic in this module

use std::sync::Arc;

use clap::Parser;
use tracing::info;

Expand Down Expand Up @@ -56,14 +58,15 @@ pub fn run() {
);

// Initialize service container for dependency injection
let container = bootstrap::Container::new();
let container = Arc::new(bootstrap::Container::new());
let context = presentation::dispatch::ExecutionContext::new(container);

match cli.command {
Some(command) => {
if let Err(e) =
presentation::execute(command, &cli.global.working_dir, &container.user_output())
presentation::dispatch::route_command(command, &cli.global.working_dir, &context)
{
presentation::handle_error(&e, &container.user_output());
presentation::handle_error(&e, &context.user_output());
std::process::exit(1);
}
}
Expand Down
49 changes: 49 additions & 0 deletions src/presentation/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@ pub mod tests;

/// Execute the given command
///
/// **DEPRECATED**: This function is deprecated in favor of the new Dispatch Layer.
/// Use `crate::presentation::dispatch::route_command` instead.
///
/// This function will be removed in a future version. The new dispatch layer
/// provides better separation of concerns and cleaner architecture.
///
/// # Migration Guide
///
/// Old code:
/// ```rust,ignore
/// use std::sync::{Arc, Mutex};
/// use crate::presentation::{commands, user_output::UserOutput};
///
/// let user_output = Arc::new(Mutex::new(UserOutput::new(/* ... */)));
/// commands::execute(command, working_dir, &user_output)?;
/// ```
///
/// New code:
/// ```rust,ignore
/// use std::sync::Arc;
/// use crate::bootstrap::Container;
/// use crate::presentation::dispatch::{route_command, ExecutionContext};
///
/// let container = Arc::new(Container::new());
/// let context = ExecutionContext::new(container);
/// route_command(command, working_dir, &context)?;
/// ```
///
/// This function serves as the central dispatcher for all CLI commands.
/// It matches the command type and delegates execution to the appropriate
/// command handler module.
Expand Down Expand Up @@ -65,6 +93,27 @@ pub mod tests;
/// }
/// }
/// ```
#[deprecated(
since = "0.1.0",
note = "Use `crate::presentation::dispatch::route_command` instead"
)]
///
/// ```rust
/// use clap::Parser;
/// use torrust_tracker_deployer_lib::presentation::{input::cli, commands, user_output};
/// use std::{path::Path, sync::{Arc, Mutex}};
///
/// let cli = cli::Cli::parse();
/// if let Some(command) = cli.command {
/// let working_dir = Path::new(".");
/// let user_output = Arc::new(Mutex::new(user_output::UserOutput::new(user_output::VerbosityLevel::Normal)));
/// let result = commands::execute(command, working_dir, &user_output);
/// match result {
/// Ok(_) => println!("Command executed successfully"),
/// Err(e) => commands::handle_error(&e, &user_output),
/// }
/// }
/// ```
pub fn execute(
command: Commands,
working_dir: &std::path::Path,
Expand Down
Loading
Loading