Skip to content

Commit

Permalink
Revert "[ci_runner] Fetch child invocations from the db instead of BE…
Browse files Browse the repository at this point in the history
…S events" (#7305)
  • Loading branch information
maggie-lou committed Aug 26, 2024
1 parent 27b7c71 commit 178d717
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 118 deletions.
6 changes: 3 additions & 3 deletions app/invocation/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ ts_library(
deps = [
"//app/components/link",
"//app/format",
"//proto:invocation_status_ts_proto",
"//proto:invocation_ts_proto",
"//proto:build_event_stream_ts_proto",
"@npm//@types/react",
"@npm//lucide-react",
"@npm//react",
Expand All @@ -110,7 +109,8 @@ ts_library(
srcs = ["child_invocations.tsx"],
deps = [
"//app/invocation:child_invocation_card",
"//proto:invocation_ts_proto",
"//app/invocation:invocation_model",
"//app/util:proto",
"@npm//@types/react",
"@npm//react",
],
Expand Down
64 changes: 27 additions & 37 deletions app/invocation/child_invocation_card.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
import React from "react";
import format from "../format/format";
import { invocation } from "../../proto/invocation_ts_proto";
import { CheckCircle, PlayCircle, XCircle, CircleSlash } from "lucide-react";
import { build_event_stream } from "../../proto/build_event_stream_ts_proto";
import { CheckCircle, PlayCircle, XCircle, CircleSlash, Timer } from "lucide-react";
import Link from "../components/link/link";
import { invocation_status } from "../../proto/invocation_status_ts_proto";

type CommandStatus = "failed" | "succeeded" | "in-progress" | "not-run";
export type CommandStatus = "failed" | "succeeded" | "in-progress" | "queued" | "not-run";

export type ChildInvocationCardProps = {
invocation: invocation.Invocation;
export type BazelCommandResult = {
status: CommandStatus;
invocation: InvocationMetadata;
durationMillis?: number;
};

export default class ChildInvocationCard extends React.Component<ChildInvocationCardProps> {
private getStatus(): CommandStatus {
const inv = this.props.invocation;
switch (inv.invocationStatus) {
case invocation_status.InvocationStatus.COMPLETE_INVOCATION_STATUS:
case invocation_status.InvocationStatus.DISCONNECTED_INVOCATION_STATUS:
return inv.bazelExitCode == "SUCCESS" ? "succeeded" : "failed";
case invocation_status.InvocationStatus.PARTIAL_INVOCATION_STATUS:
return "in-progress";
default:
return "not-run";
}
}
export type InvocationMetadata =
| build_event_stream.WorkflowConfigured.IInvocationMetadata
| build_event_stream.ChildInvocationsConfigured.IInvocationMetadata;

private isClickable(status: CommandStatus): boolean {
return status !== "not-run";
}
export type ChildInvocationCardProps = {
result: BazelCommandResult;
};

private getDurationLabel(status: CommandStatus): string {
if (status == "failed" || status == "succeeded") {
return format.durationUsec(this.props.invocation.durationUsec);
}
return "";
export default class ChildInvocationCard extends React.Component<ChildInvocationCardProps> {
private isClickable() {
return this.props.result.status !== "queued" && this.props.result.status !== "not-run";
}

private renderStatusIcon(status: CommandStatus) {
switch (status) {
private renderStatusIcon() {
switch (this.props.result.status) {
case "succeeded":
return <CheckCircle className="icon" />;
case "failed":
return <XCircle className="icon" />;
case "in-progress":
return <PlayCircle className="icon" />;
case "queued":
return <Timer className="icon" />;
case "not-run":
return <CircleSlash className="icon" />;
default:
Expand All @@ -53,16 +44,15 @@ export default class ChildInvocationCard extends React.Component<ChildInvocation
}

render() {
const status = this.getStatus();
const inv = this.props.invocation;
const command = `${inv.command} ${inv.pattern.join(" ")}`;
return (
<Link
className={`child-invocation-card status-${status} ${this.isClickable(status) ? "clickable" : ""}`}
href={this.isClickable(status) ? `/invocation/${this.props.invocation.invocationId}` : undefined}>
<div className="icon-container">{this.renderStatusIcon(status)}</div>
<div className="command">{command}</div>
<div className="duration">{this.getDurationLabel(status)}</div>
className={`child-invocation-card status-${this.props.result.status} ${this.isClickable() ? "clickable" : ""}`}
href={this.isClickable() ? `/invocation/${this.props.result.invocation.invocationId}` : undefined}>
<div className="icon-container">{this.renderStatusIcon()}</div>
<div className="command">{this.props.result.invocation.bazelCommand}</div>
<div className="duration">
{this.props.result.durationMillis !== undefined && format.durationMillis(this.props.result.durationMillis)}
</div>
</Link>
);
}
Expand Down
51 changes: 45 additions & 6 deletions app/invocation/child_invocations.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,61 @@
import React from "react";
import ChildInvocationCard from "./child_invocation_card";
import { invocation } from "../../proto/invocation_ts_proto";
import InvocationModel from "./invocation_model";
import ChildInvocationCard, { CommandStatus, InvocationMetadata } from "./child_invocation_card";
import { BazelCommandResult } from "./child_invocation_card";
import { durationToMillisWithFallback } from "../util/proto";

export type ChildInvocationProps = {
childInvocations: invocation.Invocation[];
model: InvocationModel;
};

export default class ChildInvocations extends React.Component<ChildInvocationProps> {
private getDurationMillis(invocation: InvocationMetadata): number | undefined {
const completedEvent = this.props.model.childInvocationCompletedByInvocationId.get(invocation.invocationId ?? "");
if (!completedEvent) return undefined;
return durationToMillisWithFallback(completedEvent.duration, +(completedEvent?.durationMillis ?? 0));
}

render() {
if (!this.props.childInvocations.length) return null;
const childInvocationConfiguredEvents = this.props.model.childInvocationsConfigured;
let invocations = [];
for (let i = 0; i < childInvocationConfiguredEvents.length; i++) {
const event = childInvocationConfiguredEvents[i];
for (let inv of event.invocation) {
invocations.push(inv);
}
}

const results: BazelCommandResult[] = [];
let inProgressCount = 0;
const getStatus = (invocation: InvocationMetadata): CommandStatus => {
const completedEvent = this.props.model.childInvocationCompletedByInvocationId.get(invocation.invocationId ?? "");
if (completedEvent) {
return completedEvent.exitCode === 0 ? "succeeded" : "failed";
} else if (this.props.model.finished) {
return "not-run";
} else if (inProgressCount === 0) {
// Only one command should be marked in progress; the rest should be
// marked queued.
inProgressCount++;
return "in-progress";
} else {
return "queued";
}
};

for (const invocation of invocations) {
results.push({ invocation, status: getStatus(invocation), durationMillis: this.getDurationMillis(invocation) });
}

if (!results.length) return null;

return (
<div className="child-invocations-section">
<h2>Bazel commands</h2>
<div className="subtitle">Click a command to see results.</div>
<div className="child-invocations-list">
{this.props.childInvocations.map((result) => (
<ChildInvocationCard key={result.invocationId} invocation={result} />
{results.map((result) => (
<ChildInvocationCard key={result.invocation.invocationId} result={result} />
))}
</div>
</div>
Expand Down
47 changes: 3 additions & 44 deletions app/invocation/invocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,6 @@ interface State {
runnerExecution?: execution_stats.Execution;
runnerLastExecuteOperation?: ExecuteOperation;

/*
* We only need to update the child invocation cards right when they've started and ended.
* Memoize them on the client, so we don't need to keep fetching them from the
* db in the meantime.
*/
childInvocations: invocation.Invocation[];

keyboardShortcutHandle: string;
}

Expand All @@ -87,7 +80,6 @@ export default class InvocationComponent extends React.Component<Props, State> {
inProgress: false,
error: null,
keyboardShortcutHandle: "",
childInvocations: [],
};

private timeoutRef: number = 0;
Expand All @@ -96,9 +88,6 @@ export default class InvocationComponent extends React.Component<Props, State> {
private modelChangedSubscription?: Subscription;
private runnerExecutionRPC?: CancelablePromise;

private seenChildInvocationConfiguredIds = new Set<string>();
private seenChildInvocationCompletedIds = new Set<string>();

componentWillMount() {
document.title = `Invocation ${this.props.invocationId} | BuildBuddy`;
// TODO(siggisim): Move moment configuration elsewhere
Expand Down Expand Up @@ -200,29 +189,23 @@ export default class InvocationComponent extends React.Component<Props, State> {
this.fetchRunnerExecution();
}

const fetchChildren = this.shouldFetchChildren(this.state.model);
let request = new invocation.GetInvocationRequest();
request.lookup = new invocation.InvocationLookup();
request.lookup.invocationId = this.props.invocationId;
request.lookup.fetchChildInvocations = fetchChildren;
rpcService.service
.getInvocation(request)
.then((response: invocation.GetInvocationResponse) => {
console.log(response);
if (!response.invocation || response.invocation.length === 0) {
throw new BuildBuddyError("NotFound", "Invocation not found.");
}
const inv = response.invocation[0];
const model = new InvocationModel(inv);
const model = new InvocationModel(response.invocation[0]);
// Only show the in-progress screen if we don't have any events yet.
const showInProgressScreen = model.isInProgress() && !inv.event?.length;
// Only update the child invocations if we've fetched new updates.
const childInvocations = fetchChildren ? inv.childInvocations : this.state.childInvocations;
const showInProgressScreen = model.isInProgress() && !response.invocation[0].event?.length;
this.setState({
inProgress: showInProgressScreen,
model: model,
error: null,
childInvocations: childInvocations,
});
})
.catch((error: any) => {
Expand All @@ -232,30 +215,6 @@ export default class InvocationComponent extends React.Component<Props, State> {
.finally(() => this.setState({ loading: false }));
}

shouldFetchChildren(model: InvocationModel | undefined): boolean {
if (!model) return true;
const childInvocationConfiguredEvents = model.childInvocationsConfigured;
let shouldFetch = false;

for (const event of childInvocationConfiguredEvents) {
for (let inv of event.invocation) {
if (!this.seenChildInvocationConfiguredIds.has(inv.invocationId)) {
this.seenChildInvocationConfiguredIds.add(inv.invocationId);
shouldFetch = true;
}
}
}

for (const iid of model.childInvocationCompletedByInvocationId.keys()) {
if (!this.seenChildInvocationCompletedIds.has(iid)) {
this.seenChildInvocationCompletedIds.add(iid);
shouldFetch = true;
}
}

return shouldFetch;
}

scheduleRefetch() {
clearTimeout(this.timeoutRef);
// Refetch invocation data in 3 seconds to update status.
Expand Down Expand Up @@ -504,7 +463,7 @@ export default class InvocationComponent extends React.Component<Props, State> {
)}
{!isBazelInvocation && (
<div className="container">
<ChildInvocations childInvocations={this.state.childInvocations} />
<ChildInvocations model={this.state.model} />
</div>
)}
</div>
Expand Down
50 changes: 22 additions & 28 deletions enterprise/server/cmd/ci_runner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,11 @@ func (ar *actionRunner) Run(ctx context.Context, ws *workspace) error {
return status.WrapError(err, "failed to get action to run")
}

cic := &bespb.ChildInvocationsConfigured{}
cicEvent := &bespb.BuildEvent{
Id: &bespb.BuildEventId{Id: &bespb.BuildEventId_ChildInvocationsConfigured{ChildInvocationsConfigured: &bespb.BuildEventId_ChildInvocationsConfiguredId{}}},
Payload: &bespb.BuildEvent_ChildInvocationsConfigured{ChildInvocationsConfigured: cic},
}
// If the triggering commit merges cleanly with the target branch, the runner
// will execute the configured bazel commands. Otherwise, the runner will
// exit early without running those commands and does not need to create
Expand All @@ -995,8 +1000,20 @@ func (ar *actionRunner) Run(ctx context.Context, ws *workspace) error {
InvocationId: iid,
}},
})
cic.Invocation = append(cic.Invocation, &bespb.ChildInvocationsConfigured_InvocationMetadata{
InvocationId: iid,
BazelCommand: bazelCmd,
})
cicEvent.Children = append(cicEvent.Children, &bespb.BuildEventId{
Id: &bespb.BuildEventId_ChildInvocationCompleted{ChildInvocationCompleted: &bespb.BuildEventId_ChildInvocationCompletedId{
InvocationId: iid,
}},
})
}
}
if err := ar.reporter.Publish(cicEvent); err != nil {
return nil
}

if !publishedWorkspaceStatus {
if err := ar.reporter.Publish(ar.workspaceStatusEvent()); err != nil {
Expand Down Expand Up @@ -1103,34 +1120,6 @@ func (ar *actionRunner) Run(ctx context.Context, ws *workspace) error {
for i, bazelCmd := range action.BazelCommands {
cmdStartTime := time.Now()

if i >= len(wfc.GetInvocation()) {
return status.InternalErrorf("No invocation metadata generated for bazel_commands[%d]; this should never happen", i)
}
iid := wfc.GetInvocation()[i].GetInvocationId()

cic := &bespb.ChildInvocationsConfigured{
Invocation: []*bespb.ChildInvocationsConfigured_InvocationMetadata{
{
InvocationId: iid,
BazelCommand: bazelCmd,
},
},
}
cicEvent := &bespb.BuildEvent{
Id: &bespb.BuildEventId{Id: &bespb.BuildEventId_ChildInvocationsConfigured{ChildInvocationsConfigured: &bespb.BuildEventId_ChildInvocationsConfiguredId{}}},
Payload: &bespb.BuildEvent_ChildInvocationsConfigured{ChildInvocationsConfigured: cic},
Children: []*bespb.BuildEventId{
{
Id: &bespb.BuildEventId_ChildInvocationCompleted{ChildInvocationCompleted: &bespb.BuildEventId_ChildInvocationCompletedId{
InvocationId: iid,
}},
},
},
}
if err := ar.reporter.Publish(cicEvent); err != nil {
return nil
}

// Publish a TargetConfigured event associated with the bazel command so
// that we can render artifacts associated with the "target".
targetLabel := fmt.Sprintf("bazel_commands[%d]", i)
Expand All @@ -1143,10 +1132,15 @@ func (ar *actionRunner) Run(ctx context.Context, ws *workspace) error {
Payload: &bespb.BuildEvent_Configured{Configured: &bespb.TargetConfigured{}},
})

if i >= len(wfc.GetInvocation()) {
return status.InternalErrorf("No invocation metadata generated for bazel_commands[%d]; this should never happen", i)
}

if err := provisionArtifactsDir(ws, i); err != nil {
return err
}

iid := wfc.GetInvocation()[i].GetInvocationId()
args, err := ws.bazelArgsWithCustomBazelrc(bazelCmd)
if err != nil {
return status.InvalidArgumentErrorf("failed to parse bazel command: %s", err)
Expand Down

0 comments on commit 178d717

Please sign in to comment.