Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.jenkins.plugins.pipelinegraphview.livestate;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Creates {@link LiveGraphState} entries at execution start / resume and evicts them on
* completion. Without this, entries are still created lazily by {@link LiveGraphPopulator}
* on first {@code onNewHead}, but {@code onResumed} guarantees the catch-up scan happens
* once up-front rather than at the first event after restart.
*/
@Extension
public class LiveGraphLifecycle extends FlowExecutionListener {

private static final Logger logger = LoggerFactory.getLogger(LiveGraphLifecycle.class);

@Override
public void onRunning(@NonNull FlowExecution execution) {
try {
LiveGraphRegistry.get().getOrCreate(execution);
} catch (Throwable t) {
logger.warn("pipeline-graph-view live state onRunning failed", t);

Check warning on line 26 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphLifecycle.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 25-26 are not covered by tests
}
}

@Override
public void onResumed(@NonNull FlowExecution execution) {
try {
LiveGraphState state = LiveGraphRegistry.get().getOrCreate(execution);
if (state != null) {
LiveGraphPopulator.catchUp(execution, state);
}
} catch (Throwable t) {
logger.warn("pipeline-graph-view live state onResumed failed", t);
}
}

Check warning on line 40 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphLifecycle.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 33-40 are not covered by tests

@Override
public void onCompleted(@NonNull FlowExecution execution) {
try {
LiveGraphRegistry.get().remove(execution);
} catch (Throwable t) {
logger.warn("pipeline-graph-view live state onCompleted failed", t);

Check warning on line 47 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphLifecycle.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 46-47 are not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.jenkins.plugins.pipelinegraphview.livestate;

import hudson.Extension;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.flow.GraphListener;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Extension that captures every new {@link FlowNode} across every running execution and
* feeds it to the corresponding {@link LiveGraphState}. The downstream {@code PipelineGraphApi}
* path reads a snapshot of that state instead of walking the whole execution each time.
*
* <p>We use {@link GraphListener.Synchronous} rather than the async variant because callers
* expect "once a node is a head, the next API read reflects it" — async delivery creates a
* lag window where the snapshot is behind the execution, which breaks tests that check state
* at precise trigger points and would surprise anyone hitting the REST API after an event.
* The work done under the monitor is trivial ({@code ArrayList}/{@code HashSet} additions),
* so the CPS VM thread is not meaningfully blocked. Every code path is still wrapped in
* try/catch and poisons the state on failure so a bug here can never disrupt a build.
*/
@Extension
public class LiveGraphPopulator implements GraphListener.Synchronous {

private static final Logger logger = LoggerFactory.getLogger(LiveGraphPopulator.class);

@Override
public void onNewHead(FlowNode node) {
LiveGraphState state = null;
try {
FlowExecution execution = node.getExecution();
state = LiveGraphRegistry.get().getOrCreate(execution);
if (state == null) {
return; // feature disabled or execution not a WorkflowRun
}
// Lazy initial catch-up: if the listener is seeing nodes for an execution it's
// never observed (plugin upgrade mid-build, Jenkins resume without onResumed
// firing first), the early history is already in the FlowExecution's storage.
// Backfill it once before processing this event.
if (state.size() == 0 && !state.hasSeen(node.getId())) {

Check warning on line 42 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphPopulator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 42 is only partially covered, one branch is missing
catchUp(execution, state);
}
state.addNode(node);
} catch (Throwable t) {
// A thrown exception here propagates into the CPS VM and can abort the build.
// Poison the state so subsequent reads fall back to the scanner; log the failure
// but never rethrow.
logger.warn("pipeline-graph-view live state failed; falling back to scanner", t);
if (state != null) {
state.poison();

Check warning on line 52 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphPopulator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 46-52 are not covered by tests
}
}
}

static void catchUp(FlowExecution execution, LiveGraphState state) {
try {
DepthFirstScanner scanner = new DepthFirstScanner();
scanner.setup(execution.getCurrentHeads());
for (FlowNode existing : scanner) {
state.addNode(existing);
}
Comment on lines +54 to +68
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

LiveGraphState.snapshot() assumes nodes insertion order is chronological (oldest→newest) and reverses it to approximate DepthFirstScanner (newest-first). In catchUp(), you iterate DepthFirstScanner directly and call state.addNode(existing) in that iteration order, which is already newest-first; after a restart this makes the subsequent reverse-scan produce oldest-first workspace candidates, so PipelineGraphApi#getStageNode may pick an outer workspace instead of the most-specific one. Consider collecting scanner output and adding nodes to the state in reverse (oldest→newest) to keep insertion order consistent with the live onNewHead path.

Copilot uses AI. Check for mistakes.
} catch (Throwable t) {
logger.warn("pipeline-graph-view live state catch-up failed; poisoning", t);
state.poison();

Check warning on line 66 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphPopulator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 64-66 are not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package io.jenkins.plugins.pipelinegraphview.livestate;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.jenkins.plugins.pipelinegraphview.utils.PipelineGraph;
import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepList;
import java.time.Duration;
import jenkins.util.SystemProperties;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;

/**
* Singleton holding one {@link LiveGraphState} per in-progress run.
* Entries are created on demand by the listener / lifecycle code, removed on completion,
* and otherwise bounded by a Caffeine LRU so abandoned entries (deleted runs, listener
* bugs) don't leak.
*/
public final class LiveGraphRegistry {

// IMPORTANT: keep CACHE_MAX_SIZE declared BEFORE INSTANCE. Static fields initialise in
// source order, and the instance's Caffeine builder reads CACHE_MAX_SIZE — if INSTANCE
// were declared first, CACHE_MAX_SIZE would still be 0 during construction and Caffeine
// would evict every entry immediately (maximumSize(0) means "no entries allowed").
private static final int CACHE_MAX_SIZE =
SystemProperties.getInteger(LiveGraphRegistry.class.getName() + ".size", 512);

private static final LiveGraphRegistry INSTANCE = new LiveGraphRegistry();

public static LiveGraphRegistry get() {
return INSTANCE;
}

private final Cache<String, LiveGraphState> states = Caffeine.newBuilder()
.maximumSize(CACHE_MAX_SIZE)
.expireAfterAccess(Duration.ofMinutes(30))
.build();
Comment on lines +41 to +44
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

expireAfterAccess(30m) can evict a live state while a run is still in progress (e.g. long input/sleep/semaphore with no new heads and no API reads). If that happens, the next onNewHead recreates a fresh LiveGraphState that never becomes ready (since onRunning/onResumed won't fire again), so the live-state path stays permanently disabled for that run. Consider removing the TTL for in-progress runs, switching to a much longer expireAfterWrite, or ensuring recreated states are marked ready (possibly with a safe catch-up) so eviction doesn't break functionality.

Copilot uses AI. Check for mistakes.

LiveGraphRegistry() {}

/**
* Escape hatch. Setting this system property to {@code false} makes
* {@link #snapshot(WorkflowRun)} always return {@code null}, forcing callers to use the
* scanner fallback. Useful if a regression lands in the live-state path.
*/
private static boolean disabled() {
return !SystemProperties.getBoolean(LiveGraphRegistry.class.getName() + ".enabled", true);
}
Comment thread
timja marked this conversation as resolved.
Outdated

LiveGraphState getOrCreate(FlowExecution execution) {
if (disabled()) {
return null;
}
String key = keyFor(execution);
if (key == null) {

Check warning on line 54 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 54 is only partially covered, one branch is missing
return null;

Check warning on line 55 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 55 is not covered by tests
}
return states.get(key, k -> new LiveGraphState());
}

/**
* Returns a snapshot of the live state for this run, or {@code null} if none exists
* (feature disabled, state never populated, state poisoned). Callers must treat
* {@code null} as "fall back to the scanner path."
*/
public LiveGraphSnapshot snapshot(WorkflowRun run) {
if (disabled()) {
return null;
}
LiveGraphState state = states.getIfPresent(run.getExternalizableId());
return state == null ? null : state.snapshot();
}

void remove(FlowExecution execution) {
String key = keyFor(execution);
if (key != null) {

Check warning on line 75 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 75 is only partially covered, one branch is missing
states.invalidate(key);
}
}

/**
* Returns a previously-cached {@link PipelineGraph} for this run if it was computed at
* or after {@code minVersion}, otherwise {@code null}. Use {@link LiveGraphSnapshot#version()}
* as the argument — cache entries older than the caller's snapshot are rejected.
*/
public PipelineGraph cachedGraph(WorkflowRun run, long minVersion) {
if (disabled()) {

Check warning on line 86 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 86 is only partially covered, one branch is missing
return null;

Check warning on line 87 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 87 is not covered by tests
}
LiveGraphState state = states.getIfPresent(run.getExternalizableId());
return state == null ? null : state.cachedGraph(minVersion);

Check warning on line 90 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 90 is only partially covered, one branch is missing
}

/** Stores a {@link PipelineGraph} computed from a snapshot at {@code version}. */
public void cacheGraph(WorkflowRun run, long version, PipelineGraph graph) {
if (disabled()) {

Check warning on line 95 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 95 is only partially covered, one branch is missing
return;

Check warning on line 96 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 96 is not covered by tests
}
LiveGraphState state = states.getIfPresent(run.getExternalizableId());
if (state != null) {

Check warning on line 99 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 99 is only partially covered, one branch is missing
state.cacheGraph(version, graph);
}
}

public PipelineStepList cachedAllSteps(WorkflowRun run, long minVersion) {
if (disabled()) {

Check warning on line 105 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 105 is only partially covered, one branch is missing
return null;

Check warning on line 106 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 106 is not covered by tests
}
LiveGraphState state = states.getIfPresent(run.getExternalizableId());
return state == null ? null : state.cachedAllSteps(minVersion);

Check warning on line 109 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 109 is only partially covered, one branch is missing
}

public void cacheAllSteps(WorkflowRun run, long version, PipelineStepList steps) {
if (disabled()) {

Check warning on line 113 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 113 is only partially covered, one branch is missing
return;

Check warning on line 114 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 114 is not covered by tests
}
LiveGraphState state = states.getIfPresent(run.getExternalizableId());
if (state != null) {

Check warning on line 117 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 117 is only partially covered, one branch is missing
state.cacheAllSteps(version, steps);
}
}

private static String keyFor(FlowExecution execution) {
try {
Object exec = execution.getOwner().getExecutable();
if (exec instanceof WorkflowRun run) {

Check warning on line 125 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 125 is only partially covered, one branch is missing
return run.getExternalizableId();
}
return null;
} catch (Exception e) {
return null;

Check warning on line 130 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphRegistry.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 128-130 are not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.jenkins.plugins.pipelinegraphview.livestate;

import java.util.List;
import org.jenkinsci.plugins.workflow.graph.FlowNode;

/**
* Immutable projection of a {@link LiveGraphState} at a point in time.
* Held briefly outside the state's monitor so HTTP callers can construct DTOs without
* blocking the CPS VM thread that's feeding the live state.
*
* <p>{@code version} is a monotonically-increasing counter that bumps on every new flow
* node. It lets HTTP callers key the output cache so repeat polls between node arrivals
* return the cached {@code PipelineGraph} / {@code PipelineStepList} without rebuilding.
*/
public record LiveGraphSnapshot(List<FlowNode> nodes, List<FlowNode> workspaceNodes, long version) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.jenkins.plugins.pipelinegraphview.livestate;

import io.jenkins.plugins.pipelinegraphview.utils.PipelineGraph;
import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jenkinsci.plugins.workflow.actions.WorkspaceAction;
import org.jenkinsci.plugins.workflow.graph.FlowNode;

/**
* Per-run mutable state built up by {@link LiveGraphPopulator} as {@code GraphListener}
* events arrive. Reads and writes are serialised on the instance monitor; holders should
* snapshot and release quickly — the writer is the CPS VM thread and must not block.
*
* <p>Two things live here:
* <ul>
* <li>The raw {@link FlowNode} list (plus the {@link WorkspaceAction}-carrying subset)
* that the downstream relationship-finder / graph-builder path consumes.</li>
* <li>A small cache of the last-computed {@link PipelineGraph} / {@link PipelineStepList}
* keyed by a monotonic version counter that bumps on every {@link #addNode}. HTTP
* polls that hit between node arrivals return the cached DTO verbatim — no rebuild.</li>
* </ul>
*/
final class LiveGraphState {

private final List<FlowNode> nodes = new ArrayList<>();
private final Set<String> seenIds = new HashSet<>();
private final List<FlowNode> workspaceNodes = new ArrayList<>();
private long version = 0;
private volatile boolean poisoned = false;

// Output cache. Volatile reference to an immutable (version, value) tuple so readers
// and writers never see a torn pair.
private volatile VersionedCache<PipelineGraph> cachedGraph;
private volatile VersionedCache<PipelineStepList> cachedAllSteps;

synchronized void addNode(FlowNode node) {
if (!seenIds.add(node.getId())) {
return;
}
nodes.add(node);
if (node.getAction(WorkspaceAction.class) != null) {

Check warning on line 44 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 44 is only partially covered, one branch is missing
workspaceNodes.add(node);

Check warning on line 45 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 45 is not covered by tests
}
version++;
}

synchronized boolean hasSeen(String nodeId) {
return seenIds.contains(nodeId);
}

synchronized int size() {
return nodes.size();
}

synchronized LiveGraphSnapshot snapshot() {
if (poisoned) {

Check warning on line 59 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 59 is only partially covered, one branch is missing
return null;

Check warning on line 60 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 60 is not covered by tests
}
return new LiveGraphSnapshot(List.copyOf(nodes), List.copyOf(workspaceNodes), version);
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

snapshot() is synchronized and performs List.copyOf(...) over the full node lists on every call. For large in-progress graphs and frequent polling, this is O(N) work while holding the monitor that addNode also needs, which can block the CPS VM thread. Consider memoizing the last LiveGraphSnapshot and returning it when version is unchanged, or publishing an immutable snapshot from addNode so readers can fetch it without copying under the lock.

Copilot uses AI. Check for mistakes.

/**
* Returns the cached graph if it was computed at or after {@code minVersion}. Returning
* a newer cache than requested is intentional — the caller's snapshot can only become
* staler, so a newer output is strictly more accurate.
*/
PipelineGraph cachedGraph(long minVersion) {
VersionedCache<PipelineGraph> cached = cachedGraph;
return (cached != null && cached.version >= minVersion) ? cached.value : null;
}

void cacheGraph(long version, PipelineGraph graph) {
VersionedCache<PipelineGraph> current = cachedGraph;
if (current == null || current.version < version) {

Check warning on line 77 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 77 is only partially covered, one branch is missing
cachedGraph = new VersionedCache<>(version, graph);
}
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

cacheGraph is not thread-safe: two concurrent callers can both observe the same current value and then overwrite cachedGraph out of order, allowing an older version to replace a newer one. This can cause stale PipelineGraph instances to be served even after a newer graph was cached. Make the update monotonic (e.g., synchronize cacheGraph, or use an AtomicReference/CAS loop that only replaces the cache when the stored version is still older).

Copilot uses AI. Check for mistakes.

PipelineStepList cachedAllSteps(long minVersion) {
VersionedCache<PipelineStepList> cached = cachedAllSteps;
return (cached != null && cached.version >= minVersion) ? cached.value : null;

Check warning on line 84 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 84 is only partially covered, one branch is missing
}

void cacheAllSteps(long version, PipelineStepList steps) {
VersionedCache<PipelineStepList> current = cachedAllSteps;
if (current == null || current.version < version) {

Check warning on line 89 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 89 is only partially covered, 3 branches are missing
cachedAllSteps = new VersionedCache<>(version, steps);
}
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

cacheAllSteps has the same race as cacheGraph: the check-then-set on the volatile cachedAllSteps reference can regress the cached version under concurrent requests, leading to stale step lists being served. Use synchronization or an atomic compare-and-set style update to guarantee the cached version never decreases.

Copilot uses AI. Check for mistakes.

Comment on lines +150 to +158
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The live-state caches return the same PipelineStepList instance across calls, but PipelineStepList.steps is a publicly mutable List. That means a consumer (or even accidental mutation during serialization) can corrupt the cached value and affect other concurrent readers. Consider caching an immutable/defensive copy (or wrapping steps as unmodifiable) before storing/returning it from the live-state cache.

Suggested change
return (cachedAllSteps != null && cachedAllSteps.version >= minVersion) ? cachedAllSteps.value : null;
}
synchronized void cacheAllSteps(long version, PipelineStepList steps) {
if (cachedAllSteps == null || cachedAllSteps.version < version) {
cachedAllSteps = new VersionedCache<>(version, steps);
}
}
return (cachedAllSteps != null && cachedAllSteps.version >= minVersion)
? copyPipelineStepList(cachedAllSteps.value)
: null;
}
synchronized void cacheAllSteps(long version, PipelineStepList steps) {
if (cachedAllSteps == null || cachedAllSteps.version < version) {
cachedAllSteps = new VersionedCache<>(version, copyPipelineStepList(steps));
}
}
private PipelineStepList copyPipelineStepList(PipelineStepList steps) {
return new PipelineStepList(List.copyOf(steps.steps));
}

Copilot uses AI. Check for mistakes.
void poison() {
poisoned = true;
}

Check warning on line 96 in src/main/java/io/jenkins/plugins/pipelinegraphview/livestate/LiveGraphState.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 95-96 are not covered by tests

private record VersionedCache<T>(long version, T value) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import io.jenkins.plugins.pipelinegraphview.utils.PipelineGraphBuilderApi;
import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepBuilderApi;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -34,6 +36,14 @@ public PipelineNodeGraphAdapter(WorkflowRun run) {
treeScanner = new PipelineNodeTreeScanner(run);
}

/**
* Builds the adapter over a pre-collected node set rather than walking the execution
* graph. Used by the live-state path.
*/
public PipelineNodeGraphAdapter(WorkflowRun run, Collection<FlowNode> preCollectedNodes) {
treeScanner = new PipelineNodeTreeScanner(run, preCollectedNodes);
}

private final Object pipelineLock = new Object();
private final Object stepLock = new Object();
private final Object remapLock = new Object();
Expand Down
Loading
Loading