owned this note
owned this note
Published
Linked with GitHub
# Composite Content View Auto-Publish Chaining Feature
## Overview
The composite CV chaining feature solves race conditions and ensures optimal coordination when multiple component content views trigger composite publishes simultaneously. It uses an event-based approach with Dynflow task chaining for sibling coordination.
## Core Components
- **Event Queue**: All composite publishes are triggered via events for consistency
- **Dynflow Task Chaining**: Coordinates concurrent component CV publishes via event handler
- **Status Detection**: Intelligent checking of scheduled/running states
- **Sibling Task Coordination**: Ensures all component CVs complete before composite publish
---
## High-Level Flow Diagram
```mermaid
graph TD
A[Component CV Completes Publishing] --> B[Enter Finalize Phase]
B --> C[auto_publish_composites! called]
C --> D{Check Composite Status}
D -->|:scheduled| E[Skip]
D -->|:running or nil| F[Schedule Event]
F --> G[Event Queued]
G -.->|Async polling| I[Event Handler Runs]
I --> J[AutoPublishCompositeView.run]
J --> K{Find Running Sibling Tasks?}
K -->|Yes| L[Chain Composite to Siblings]
K -->|No| M[Execute Composite Immediately]
L --> N[Wait for Siblings to Complete]
N --> O[Composite CV Publishes]
M --> O
O --> P{Lock Conflict?}
P -->|Yes| Q[Set retry=true]
Q -.-> I
P -->|No| R[Done]
```
**Decision Points:**
1. **Composite Status Check** - `:scheduled` → skip | `:running` or `nil` → schedule event
2. **Sibling Task Check** - Siblings found → chain (wait) | No siblings → execute immediately
3. **Lock Conflict Check** - Conflict → retry event | Success → done
---
## Key Scenarios
### Scenario 1: Simple Case (No Activity)
**Component A publishes → No composite activity**
- Status: `nil` → Schedule event
- Event fires: No siblings → Execute immediately
- Result: Composite publishes with A's new version
### Scenario 2: Sibling Coordination
**Component A finishes → Component B & C still publishing**
- Status: `nil` → Schedule event with A's task_id
- Event fires: Finds B & C running (excludes A)
- Result: Composite chains to B & C, publishes after they complete
### Scenario 3: Composite Running
**Component A publishes → Composite C already running**
- Status: `:running` → Schedule event
- Event fires after C completes (180s retry if needed)
- Result: New composite publish with A's latest version
### Scenario 4: Already Scheduled
**Component A publishes → Composite already scheduled**
- Status: `:scheduled` → Skip
- Result: Scheduled task uses latest component versions when it plans
### Scenario 5: Complex - Event with Siblings
**A publishes → C runs → A publishes again (running) → B publishes**
1. B finishes, checks status: `:running` → schedules event with B's task_id
2. Event fires, finds A still running
3. Excludes B (via calling_task_id), chains C to A only
4. Result: C publishes with both A's and B's new versions
---
## Technical Implementation
### Entry Point: auto_publish_composites!
```ruby
def auto_publish_composites!(component_task_id)
case composite_publish_status(composite_cv)
when :scheduled
# Skip - scheduled task will use latest versions when it plans
Rails.logger.info("Composite already scheduled, skipping")
next
when :running, nil
# Always schedule event (unified code path)
schedule_auto_publish_event(composite_cv, description, component_task_id)
end
end
```
### Event Handler: Coordination Logic
```ruby
def run
::Katello::ContentViewVersion.trigger_composite_publish_with_coordination(
composite_view,
metadata[:description],
metadata[:version_id],
calling_task_id: metadata[:calling_task_id]
)
rescue ForemanTasks::Lock::LockConflict => e
self.retry = true # Event will retry automatically
raise e
end
```
### Coordination: Sibling Detection & Chaining
```ruby
def self.trigger_composite_publish_with_coordination(composite_cv, description, triggered_by_version_id, calling_task_id: nil)
# Find running component CV publish tasks
component_cv_ids = composite_cv.components.pluck(:content_view_id)
running_tasks = ForemanTasks::Task::DynflowTask
.for_action(::Actions::Katello::ContentView::Publish)
.where(state: ['planning', 'planned', 'running'])
.select { |task| component_cv_ids.include?(task.input.dig('content_view', 'id')) }
sibling_task_ids = running_tasks.map(&:external_id)
# Exclude the calling component task to avoid self-dependency
sibling_task_ids.reject! { |id| id == calling_task_id } if calling_task_id
# Chain if siblings exist, otherwise execute immediately
if sibling_task_ids.any?
ForemanTasks.dynflow.world.chain(sibling_task_ids, ::Actions::Katello::ContentView::Publish, ...)
else
ForemanTasks.async_task(::Actions::Katello::ContentView::Publish, ...)
end
end
```
---
## Benefits
1. **Unified Code Path**: All composite publishes go through events for consistency
2. **Race Condition Prevention**: Dynflow chaining ensures proper sibling coordination
3. **Resource Optimization**: Avoids unnecessary duplicate publishes via status detection
4. **Data Consistency**: Ensures composite includes all latest component versions
5. **Fault Tolerance**: Event-based retry handles lock conflicts gracefully with automatic retry
6. **Observability**: Clear logging and status tracking throughout the process
---
## State Transitions
### Component CV Perspective
```
Publishing → Finalize → auto_publish_composites! → Event Scheduled → Complete
```
### Composite CV Perspective (via Event)
```
Event Queued → Event Fires → Check Siblings → [Chain OR Execute] → Publishing → Complete
↓
Lock Conflict? → Retry
```
---
## Key Files
- `app/models/katello/content_view_version.rb`: Main coordination logic and event scheduling
- `app/models/katello/events/auto_publish_composite_view.rb`: Event handler with sibling coordination
- `app/lib/actions/katello/content_view/publish.rb`: Publish action that triggers auto_publish_composites!
---
*Updated 2025-11-17 for composite CV chaining feature (Issue #38856)*