Class: ChronoForge::Dashboard::BranchPresenter

Inherits:
Object
  • Object
show all
Defined in:
app/presenters/chrono_forge/dashboard/branch_presenter.rb

Overview

Health of a single branch (a branch$ execution log) for the parent's detail page. Every child count is CAPPED and index-only on (parent_execution_log_id, state) — a branch can hold hundreds of thousands of children, so we never count the full set, only up to CAP (shown "CAP+").

Constant Summary collapse

CAP =
5000
BLOCKED_STATES =
%i[failed stalled].map { |s| ChronoForge::Workflow.states[s] }.freeze
POLL_OVERDUE_GRACE =

A scheduled next poll this far past due means the BranchMergeJob poller likely never ran (queue latency aside) — a heuristic, hence "potential".

120

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(log, merge_state = nil) ⇒ BranchPresenter

merge_state: :merged | :merging | nil (not yet merged)



11
12
13
14
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 11

def initialize(log, merge_state = nil)
  @log = log
  @merge_state = merge_state
end

Instance Attribute Details

#logObject (readonly)

Returns the value of attribute log.



16
17
18
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 16

def log
  @log
end

#merge_stateObject (readonly)

Returns the value of attribute merge_state.



16
17
18
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 16

def merge_state
  @merge_state
end

Instance Method Details

#blockedObject



27
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 27

def blocked = capped(children.where(state: BLOCKED_STATES))

#capObject



29
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 29

def cap = CAP

#eta_secondsObject



59
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 59

def eta_seconds = poll&.dig("eta_seconds")

#exact_never_startedObject



73
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 73

def exact_never_started = poll&.dig("never_started")

#exact_pendingObject

The FULL (uncapped) pending / never-started counts the poller ALREADY records each pass — so the dashboard can show the real number instead of the capped "CAP+", with NO new query. nil until the branch has been polled; callers fall back to the capped count. (The poll's "never_started" is the never-started count.)



71
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 71

def exact_pending = poll&.dig("pending")

#exact_spawnedObject

Total spawned, counted once and cached by the poller when the branch sealed (see BranchProbe#spawned) — exact and free. nil until sealed+polled; the "Spawned" column then falls back to the capped live count.



78
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 78

def exact_spawned = poll&.dig("spawned")

#last_polled_atObject



41
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 41

def last_polled_at = parse_time(poll&.dig("last_polled_at"))

#last_rekick_atObject



84
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 84

def last_rekick_at = parse_time(poll&.dig("last_rekick_at"))

#nameObject



18
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 18

def name = StepNameParser.parse(@log.step_name).name

#never_startedObject

Children dispatched but not yet started (idle, started_at nil) — how much of this branch hasn't been picked up yet. Capped/index-only like the other counts.



65
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 65

def never_started = capped(children.where(state: ChronoForge::Workflow.states[:idle], started_at: nil))

#next_poll_atObject



43
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 43

def next_poll_at = parse_time(poll&.dig("next_poll_at"))

#pendingObject



25
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 25

def pending = capped(children.where.not(state: ChronoForge::Workflow.states[:completed]))

#poll_overdue?Boolean

next_poll_at is nil once the merge completes, so a finished merge never looks overdue; a non-nil time well in the past = the poller is likely dead.

Returns:

  • (Boolean)


49
50
51
52
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 49

def poll_overdue?
  t = next_poll_at
  t.present? && t < Time.current - POLL_OVERDUE_GRACE
end

#polled?Boolean

The BranchMergeJob stamps its poll state onto the branch log's metadata (it can't be queried from the backend; ActiveJob has no such API).

Returns:

  • (Boolean)


39
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 39

def polled? = poll.present?

#pollsObject



45
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 45

def polls = poll&.dig("polls").to_i

#rateObject

Live throughput/ETA from the last poll (the poller measures the branch's completion rate each pass). The wake poll records rate 0, so a positive rate means the branch is still draining — a merged/idle branch shows no gauge.



57
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 57

def rate = poll&.dig("rate").to_f

#rekicksObject

Dropped-child recovery: how many children the poller has rekicked, and when it last did (nil if never).



82
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 82

def rekicks = poll&.dig("rekick_total").to_i

#sealed?Boolean

The branch is "sealed" once its block closed (done dispatching children).

Returns:

  • (Boolean)


21
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 21

def sealed? = @log.completed?

#spawnedObject



23
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 23

def spawned = capped(children)

#throughput?Boolean

Returns:

  • (Boolean)


61
# File 'app/presenters/chrono_forge/dashboard/branch_presenter.rb', line 61

def throughput? = rate > 0