# Custom Task Services Development Guide

Develop custom task services for domain-specific infrastructure operations.

## Task Service Architecture

A task service is a self-contained Rust microservice that executes infrastructure operations:

```text
Provisioning Orchestrator
        ↓
    Task Execution Engine
        ↓
    Service Registry
        ├─ Kubernetes Deployer
        ├─ AWS EC2 Manager
        ├─ PostgreSQL Manager
        ├─ Custom Task Service ← You are here
        └─ ...
        ↓
    Your Custom Service
        ├─ Execute operation
        ├─ Update status
        └─ Return results
```

## Minimal Task Service Template

```rust
// custom-services/my-service/src/lib.rs
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecutionContext {
    pub operation_id: String,
    pub workspace_id: String,
    pub parameters: serde_json::Value,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecutionResult {
    pub operation_id: String,
    pub status: String,  // "success" | "failure" | "running"
    pub output: serde_json::Value,
    pub error: Option<String>,
}

pub struct MyCustomService {
    state: Arc<RwLock<serde_json::Value>>,
}

impl MyCustomService {
    pub fn new() -> Self {
        Self {
            state: Arc::new(RwLock::new(serde_json::json!({}))),
        }
    }

    pub async fn execute(&self, context: ExecutionContext) -> ExecutionResult {
        match context.parameters.get("action").and_then| ( | v v.as_str()) {
            Some("deploy") => self.handle_deploy(context).await,
            Some("destroy") => self.handle_destroy(context).await,
            Some("status") => self.handle_status(context).await,
            _ => ExecutionResult {
                operation_id: context.operation_id,
                status: "failure".to_string(),
                output: serde_json::json!({}),
                error: Some("Unknown action".to_string()),
            },
        }
    }

    async fn handle_deploy(&self, context: ExecutionContext) -> ExecutionResult {
        let mut state = self.state.write().await;

        // Your custom logic here
        let result = serde_json::json!({
            "deployed_resources": 5,
            "deployment_time_ms": 2345
        });

        state["last_deployment"] = result.clone();

        ExecutionResult {
            operation_id: context.operation_id,
            status: "success".to_string(),
            output: result,
            error: None,
        }
    }

    async fn handle_destroy(&self, context: ExecutionContext) -> ExecutionResult {
        let mut state = self.state.write().await;
        state.as_object_mut().unwrap().clear();

        ExecutionResult {
            operation_id: context.operation_id,
            status: "success".to_string(),
            output: serde_json::json!({}),
            error: None,
        }
    }

    async fn handle_status(&self, context: ExecutionContext) -> ExecutionResult {
        let state = self.state.read().await;

        ExecutionResult {
            operation_id: context.operation_id,
            status: "success".to_string(),
            output: state.clone(),
            error: None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_deploy() {
        let service = MyCustomService::new();
        let context = ExecutionContext {
            operation_id: "op-123".to_string(),
            workspace_id: "ws-456".to_string(),
            parameters: serde_json::json!({ "action": "deploy" }),
        };

        let result = service.execute(context).await;
        assert_eq!(result.status, "success");
    }
}
```

## HTTP API Service

Expose task service as REST API:

```rust
// custom-services/my-service/src/main.rs
use actix_web::{web, App, HttpServer, HttpResponse};
use serde_json::json;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let service = web::Data::new(MyCustomService::new());

    HttpServer::new(move | | {
        App::new()
            .app_data(service.clone())
            .route("/health", web::get().to(health_check))
            .route("/execute", web::post().to(execute_task))
            .route("/status", web::get().to(get_status))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

async fn health_check() -> HttpResponse {
    HttpResponse::Ok().json(json!({
        "status": "healthy",
        "service": "my-custom-service",
        "version": "1.0.0"
    }))
}

async fn execute_task(
    service: web::Data<MyCustomService>,
    context: web::Json<ExecutionContext>,
) -> HttpResponse {
    let result = service.execute(context.into_inner()).await;
    HttpResponse::Ok().json(result)
}

async fn get_status(service: web::Data<MyCustomService>) -> HttpResponse {
    let context = ExecutionContext {
        operation_id: "status-check".to_string(),
        workspace_id: "".to_string(),
        parameters: json!({"action": "status"}),
    };

    let result = service.execute(context).await;
    HttpResponse::Ok().json(result)
}
```

## Register Service with Provisioning

```nushell
# Register task service
provisioning service register \
    --name "my-custom-service" \
    --version "1.0.0" \
    --endpoint " [http://my-service:8080"](http://my-service:8080") \
    --capabilities ["deploy", "destroy", "status"] \
    --health-check "/health"

# Verify registration
provisioning service list --type custom
```

## Invoking Custom Service from Workflow

```nickel
{
  workflows.custom_workflow = {
    tasks = [
      {
        name = "custom_deploy",
        type = "my-custom-service",
        action = "deploy",
        parameters = {
          resource_type = "database",
          config = {
            engine = "postgres",
            version = "15"
          }
        }
      },
      {
        name = "check_status",
        type = "my-custom-service",
        action = "status",
        depends_on = ["custom_deploy"]
      }
    ]
  }
}
```

## Error Handling

```rust
pub async fn execute_with_retry(&self, context: ExecutionContext) -> ExecutionResult {
    let max_retries = 3;
    let mut attempts = 0;

    loop {
        match self.execute_internal(&context).await {
            Ok(result) => return result,
            Err(e) => {
                attempts += 1;
                if attempts >= max_retries {
                    return ExecutionResult {
                        operation_id: context.operation_id.clone(),
                        status: "failure".to_string(),
                        output: serde_json::json!({}),
                        error: Some(format!("Failed after {} attempts: {}", max_retries, e)),
                    };
                }

                let backoff = Duration::from_secs(2_u64.pow(attempts as u32));
                tokio::time::sleep(backoff).await;
            }
        }
    }
}
```

## Monitoring and Observability

```rust
use prometheus::{Counter, Histogram};

pub struct MyCustomService {
    execute_counter: Counter,
    execute_duration: Histogram,
}

impl MyCustomService {
    pub fn new() -> Self {
        Self {
            execute_counter: Counter::new(
                "custom_service_executions_total",
                "Total executions"
            ).unwrap(),
            execute_duration: Histogram::new(
                "custom_service_execution_duration_seconds",
                "Execution duration"
            ).unwrap(),
        }
    }

    pub async fn execute(&self, context: ExecutionContext) -> ExecutionResult {
        self.execute_counter.inc();
        let timer = self.execute_duration.start_timer();

        let result = self.execute_internal(context).await;

        timer.observe_duration();
        result
    }
}
```

## Testing Custom Services

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_deploy_success() {
        let service = MyCustomService::new();
        let context = ExecutionContext {
            operation_id: "op-1".to_string(),
            workspace_id: "ws-1".to_string(),
            parameters: json!({"action": "deploy"}),
        };

        let result = service.execute(context).await;
        assert_eq!(result.status, "success");
        assert!(result.error.is_none());
    }

    #[tokio::test]
    async fn test_unknown_action() {
        let service = MyCustomService::new();
        let context = ExecutionContext {
            operation_id: "op-1".to_string(),
            workspace_id: "ws-1".to_string(),
            parameters: json!({"action": "unknown"}),
        };

        let result = service.execute(context).await;
        assert_eq!(result.status, "failure");
        assert!(result.error.is_some());
    }

    #[tokio::test]
    async fn test_concurrent_executions() {
        let service = Arc::new(MyCustomService::new());
        let mut handles = vec![];

        for i in 0..10 {
            let service = service.clone();
            let handle = tokio::spawn(async move {
                let context = ExecutionContext {
                    operation_id: format!("op-{}", i),
                    workspace_id: "ws-1".to_string(),
                    parameters: json!({"action": "deploy"}),
                };
                service.execute(context).await
            });
            handles.push(handle);
        }

        for handle in handles {
            let result = handle.await.unwrap();
            assert_eq!(result.status, "success");
        }
    }
}
```

## Deployment

```dockerfile
# custom-services/my-service/Dockerfile
FROM rust:latest as builder

WORKDIR /build
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /build/target/release/my-service /usr/local/bin/

EXPOSE 8080
CMD ["my-service"]
```

```yaml
# custom-services/my-service/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-custom-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-custom-service

  template:
    metadata:
      labels:
        app: my-custom-service

    spec:
      containers:
      - name: service
        image: my-registry/my-custom-service:1.0.0
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 30
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 1000m
            memory: 512Mi
```

## See Also

- [Task Services Guide](../infrastructure/task-services-guide.md)
- [Batch Workflow System](../infrastructure/batch-workflows.md)
- [Testing Infrastructure](./testing-infrastructure.md)
