[ResultDB] Implement workunits.ReadRealms and ReadBatch. Clean-up.
Provide a way to batch read realms and work units in preparation
for implementing BatchGetWorkUnits.
Cleans-up handling of instruction names in test code and in creation.
BUG=b:422274371
Change-Id: I8be35d6b428e33bdc1f27914c32be83c80911b1b
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/6726494
Commit-Queue: Patrick Meiring <meiring@google.com>
Reviewed-by: Beining Chen <beining@google.com>
diff --git a/resultdb/internal/rootinvocations/read_test.go b/resultdb/internal/rootinvocations/read_test.go
index 66948ca..b25b55a 100644
--- a/resultdb/internal/rootinvocations/read_test.go
+++ b/resultdb/internal/rootinvocations/read_test.go
@@ -33,15 +33,30 @@
ctx := testutil.SpannerTestContext(t)
const realm = "testproject:testrealm"
+
+ // Prepare a root invocation with all fields set.
const id = ID("root-inv-id")
testData := NewBuilder(id).WithRealm(realm).Build()
- testutil.MustApply(ctx, t, InsertForTesting(testData)...)
+ ms := InsertForTesting(testData)
+
+ // Prepare a root invocation with minimal fields set.
+ const idMinimal = ID("root-inv-id-minimal")
+ testDataMinimal := NewBuilder("root-inv-id-minimal").WithRealm(realm).WithMinimalFields().Build()
+ ms = append(ms, InsertForTesting(testDataMinimal)...)
+ testutil.MustApply(ctx, t, ms...)
t.Run("Read", func(t *ftt.Test) {
t.Run("happy path", func(t *ftt.Test) {
- row, err := Read(span.Single(ctx), id)
- assert.Loosely(t, err, should.BeNil)
- assert.That(t, row, should.Match(&testData))
+ t.Run("maximal fields", func(t *ftt.Test) {
+ row, err := Read(span.Single(ctx), id)
+ assert.Loosely(t, err, should.BeNil)
+ assert.That(t, row, should.Match(testData))
+ })
+ t.Run("minimal fields", func(t *ftt.Test) {
+ row, err := Read(span.Single(ctx), idMinimal)
+ assert.Loosely(t, err, should.BeNil)
+ assert.That(t, row, should.Match(testDataMinimal))
+ })
})
t.Run("not found", func(t *ftt.Test) {
diff --git a/resultdb/internal/rootinvocations/span.go b/resultdb/internal/rootinvocations/span.go
index cd2710f..3afc383 100644
--- a/resultdb/internal/rootinvocations/span.go
+++ b/resultdb/internal/rootinvocations/span.go
@@ -20,6 +20,7 @@
"time"
"cloud.google.com/go/spanner"
+ "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"go.chromium.org/luci/resultdb/internal/invocations"
@@ -97,6 +98,24 @@
Submitted bool
}
+// Clone makes a deep copy of the row.
+func (r *RootInvocationRow) Clone() *RootInvocationRow {
+ ret := *r
+ if r.Tags != nil {
+ ret.Tags = make([]*pb.StringPair, len(r.Tags))
+ for i, tp := range r.Tags {
+ ret.Tags[i] = proto.Clone(tp).(*pb.StringPair)
+ }
+ }
+ if r.Properties != nil {
+ ret.Properties = proto.Clone(r.Properties).(*structpb.Struct)
+ }
+ if r.Sources != nil {
+ ret.Sources = proto.Clone(r.Sources).(*pb.Sources)
+ }
+ return &ret
+}
+
// Convert the root invocation row to the canonical form.
func (r *RootInvocationRow) Normalize() {
pbutil.SortStringPairs(r.Tags)
diff --git a/resultdb/internal/rootinvocations/span_test.go b/resultdb/internal/rootinvocations/span_test.go
index bd54b1e..ab6b580 100644
--- a/resultdb/internal/rootinvocations/span_test.go
+++ b/resultdb/internal/rootinvocations/span_test.go
@@ -21,7 +21,6 @@
"cloud.google.com/go/spanner"
"google.golang.org/protobuf/proto"
- "google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/clock/testclock"
@@ -43,37 +42,9 @@
now := testclock.TestRecentTimeUTC
ctx, _ = testclock.UseTime(ctx, now)
- properties := &structpb.Struct{
- Fields: map[string]*structpb.Value{
- "key": structpb.NewStringValue("value"),
- },
- }
- sources := &pb.Sources{
- GitilesCommit: &pb.GitilesCommit{
- Host: "chromium.googlesource.com",
- Project: "chromium/src",
- Ref: "refs/heads/main",
- CommitHash: "1234567890abcdef1234567890abcdef12345678",
- Position: 123,
- },
- }
id := "root-inv-id"
- row := &RootInvocationRow{
- RootInvocationID: ID(id),
- State: pb.RootInvocation_ACTIVE,
- Realm: "testproject:testrealm",
- CreatedBy: "user:test@example.com",
- Deadline: now.Add(2 * 24 * time.Hour),
- UninterestingTestVerdictsExpirationTime: spanner.NullTime{Valid: true, Time: now.Add(2 * 24 * time.Hour)},
- CreateRequestID: "test-request-id",
- ProducerResource: "//builds.example.com/builds/123",
- Tags: pbutil.StringPairs("k2", "v2", "k1", "v1"),
- Properties: properties,
- Sources: sources,
- IsSourcesFinal: true,
- BaselineID: "try:linux-rel",
- Submitted: false,
- }
+ row := NewBuilder("root-inv-id").WithState(pb.RootInvocation_ACTIVE).Build()
+
commitTime, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
mutations := Create(row)
span.BufferWrite(ctx, mutations...)
@@ -90,7 +61,7 @@
assert.Loosely(t, err, should.BeNil)
row.CreateTime = commitTime
row.SecondaryIndexShardID = row.RootInvocationID.shardID(secondaryIndexShardCount)
- assert.Loosely(t, readRootInv, should.Match(row))
+ assert.That(t, readRootInv, should.Match(row))
// Validate Legacy Invocations table entry.
legacyInvID := invocations.ID(fmt.Sprintf("root:%s", id))
diff --git a/resultdb/internal/rootinvocations/testdata.go b/resultdb/internal/rootinvocations/testdata.go
index bf0aa0a..dd9c455 100644
--- a/resultdb/internal/rootinvocations/testdata.go
+++ b/resultdb/internal/rootinvocations/testdata.go
@@ -18,7 +18,6 @@
"time"
"cloud.google.com/go/spanner"
- "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"go.chromium.org/luci/resultdb/internal/spanutil"
@@ -36,6 +35,7 @@
func NewBuilder(id ID) *Builder {
return &Builder{
row: RootInvocationRow{
+ // Set all fields by default. This helps optimise test coverage.
RootInvocationID: id,
SecondaryIndexShardID: id.shardID(secondaryIndexShardCount),
State: pb.RootInvocation_FINALIZED,
@@ -65,11 +65,34 @@
},
IsSourcesFinal: true,
BaselineID: "baseline",
- Submitted: false,
+ Submitted: true,
},
}
}
+// WithMinimalFields clears as many fields as possible on the root invocation while
+// keeping it valid. This is useful for testing null and empty value handling.
+func (b *Builder) WithMinimalFields() *Builder {
+ b.row = RootInvocationRow{
+ RootInvocationID: b.row.RootInvocationID,
+ SecondaryIndexShardID: b.row.SecondaryIndexShardID,
+ // Means the finalized time and start time will be cleared in Build() unless state is
+ // subsequently overridden.
+ State: pb.RootInvocation_ACTIVE,
+ Realm: b.row.Realm,
+ CreateTime: b.row.CreateTime,
+ CreatedBy: b.row.CreatedBy,
+ FinalizeStartTime: b.row.FinalizeStartTime,
+ FinalizeTime: b.row.FinalizeTime,
+ Deadline: b.row.Deadline,
+ CreateRequestID: b.row.CreateRequestID,
+ // Prefer to use empty slice rather than nil (even though semantically identical)
+ // as this what we always report in reads.
+ Tags: []*pb.StringPair{},
+ }
+ return b
+}
+
// WithRootInvocationID sets the root invocation ID.
func (b *Builder) WithRootInvocationID(id ID) *Builder {
b.row.RootInvocationID = id
@@ -174,18 +197,11 @@
}
// Build returns the constructed RootInvocationRow.
-func (b *Builder) Build() RootInvocationRow {
- // Return a copy.
- r := b.row
- if r.Tags != nil {
- r.Tags = append([]*pb.StringPair(nil), r.Tags...)
- }
- if r.Properties != nil {
- r.Properties = proto.Clone(r.Properties).(*structpb.Struct)
- }
- if r.Sources != nil {
- r.Sources = proto.Clone(r.Sources).(*pb.Sources)
- }
+func (b *Builder) Build() *RootInvocationRow {
+ // Return a copy to avoid changes to the returned row
+ // flowing back into the builder.
+ r := b.row.Clone()
+
if r.State == pb.RootInvocation_ACTIVE {
r.FinalizeStartTime = spanner.NullTime{}
r.FinalizeTime = spanner.NullTime{}
@@ -198,7 +214,7 @@
// InsertForTesting inserts the rootInvocation record and all the
// RootInvocationShards records for a root invocation for testing purposes.
-func InsertForTesting(r RootInvocationRow) []*spanner.Mutation {
+func InsertForTesting(r *RootInvocationRow) []*spanner.Mutation {
ms := make([]*spanner.Mutation, 0, 16+1) // 16 shard and 1 root invocation
ms = append(ms, spanutil.InsertMap("RootInvocations", map[string]any{
"RootInvocationId": r.RootInvocationID,
diff --git a/resultdb/internal/services/resultdb/get_work_unit.go b/resultdb/internal/services/resultdb/get_work_unit.go
index ca64453..02464f1 100644
--- a/resultdb/internal/services/resultdb/get_work_unit.go
+++ b/resultdb/internal/services/resultdb/get_work_unit.go
@@ -67,8 +67,13 @@
return nil, appstatus.BadRequest(err)
}
+ readMask := workunits.ExcludeExtendedProperties
+ if in.View == pb.WorkUnitView_WORK_UNIT_VIEW_FULL && accessLevel == permissions.FullAccess {
+ readMask = workunits.AllFields
+ }
+
// Read the work unit.
- wu, err := workunits.Read(ctx, id)
+ wu, err := workunits.Read(ctx, id, readMask)
if err != nil {
return nil, err
}
diff --git a/resultdb/internal/services/resultdb/get_work_unit_test.go b/resultdb/internal/services/resultdb/get_work_unit_test.go
index ba157d6..f467867 100644
--- a/resultdb/internal/services/resultdb/get_work_unit_test.go
+++ b/resultdb/internal/services/resultdb/get_work_unit_test.go
@@ -26,7 +26,6 @@
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
- "go.chromium.org/luci/resultdb/internal/instructionutil"
"go.chromium.org/luci/resultdb/internal/rootinvocations"
"go.chromium.org/luci/resultdb/internal/testutil"
"go.chromium.org/luci/resultdb/internal/testutil/insert"
@@ -64,6 +63,7 @@
childWu := workunits.NewBuilder(rootInvID, childWuID.WorkUnitID).
WithRealm(wuRealm).
WithParentWorkUnitID(rootWorkUnitID.WorkUnitID).
+ WithMinimalFields().
Build()
testutil.MustApply(ctx, t, insert.WorkUnit(childWu)...)
@@ -97,7 +97,7 @@
ProducerResource: wu.ProducerResource,
Tags: wu.Tags,
Properties: wu.Properties,
- Instructions: instructionutil.InstructionsWithNames(wu.Instructions, rootWorkUnitID.Name()),
+ Instructions: wu.Instructions,
IsMasked: false,
}
diff --git a/resultdb/internal/spanutil/init_db.sql b/resultdb/internal/spanutil/init_db.sql
index e38b331..cbb71d2 100644
--- a/resultdb/internal/spanutil/init_db.sql
+++ b/resultdb/internal/spanutil/init_db.sql
@@ -82,11 +82,11 @@
-- A serialized then compressed google.protobuf.Struct that stores structured,
-- domain-specific properties of the root invocation.
-- See spanutil.Compressed type for details of compression.
- Properties BYTES(MAX) NOT NULL,
+ Properties BYTES(MAX),
-- A serialized luci.resultdb.v1.Sources message describing the source information for the
-- root invocation.
- Sources BYTES(MAX) NOT NULL,
+ Sources BYTES(MAX),
-- Whether the root invocation's source information (denoted by 'Sources') is immutable.
-- Setting this early is desirable as it enables test result exports from work units
@@ -214,7 +214,7 @@
-- exports.
--
-- See RootInvocations.Sources for more information.
- Sources BYTES(MAX) NOT NULL,
+ Sources BYTES(MAX),
-- Replica of the root invocation field, to avoid hotspotting the root invocation
-- in operations that don't need the whole root invocation, such as low-latency
@@ -304,16 +304,16 @@
-- A serialized then compressed google.protobuf.Struct that stores structured,
-- domain-specific properties of the invocation.
-- See spanutil.Compressed type for details of compression.
- Properties BYTES(MAX) NOT NULL,
+ Properties BYTES(MAX),
-- A serialized luci.resultdb.v1.Instructions describing instructions for this invocation.
-- It may contains instructions for steps (for build-level invocation) and test results.
-- It may contain instructions to test results directly contained in this invocation,
-- and test results in included invocations.
- Instructions BYTES(MAX) NOT NULL,
+ Instructions BYTES(MAX),
-- A compressed, serialized luci.resultdb.internal.invocations.ExtendedProperties message.
- ExtendedProperties BYTES(MAX) NOT NULL,
+ ExtendedProperties BYTES(MAX),
) PRIMARY KEY (RootInvocationShardId, WorkUnitId),
INTERLEAVE IN PARENT RootInvocationShards ON DELETE CASCADE;
diff --git a/resultdb/internal/testutil/insert/insert.go b/resultdb/internal/testutil/insert/insert.go
index ed4e12a..a673837 100644
--- a/resultdb/internal/testutil/insert/insert.go
+++ b/resultdb/internal/testutil/insert/insert.go
@@ -489,11 +489,11 @@
}
// RootInvocation returns Spanner mtuations to create the given root invocation.
-func RootInvocation(row rootinvocations.RootInvocationRow) []*spanner.Mutation {
+func RootInvocation(row *rootinvocations.RootInvocationRow) []*spanner.Mutation {
return rootinvocations.InsertForTesting(row)
}
// WorkUnit returns Spanner mutations to create the given work unit.
-func WorkUnit(row workunits.WorkUnitRow) []*spanner.Mutation {
+func WorkUnit(row *workunits.WorkUnitRow) []*spanner.Mutation {
return workunits.InsertForTesting(row)
}
diff --git a/resultdb/internal/testvariants/query.go b/resultdb/internal/testvariants/query.go
index 3ed4e5d..e829cf4 100644
--- a/resultdb/internal/testvariants/query.go
+++ b/resultdb/internal/testvariants/query.go
@@ -1176,7 +1176,7 @@
"passedOrSkippedVerdictEff": int(statusV2EffectivePassedOrSkipped),
"exoneratedOverride": int(pb.TestVerdict_EXONERATED),
- "notOverriden": int(pb.TestVerdict_NOT_OVERRIDDEN),
+ "notOverridden": int(pb.TestVerdict_NOT_OVERRIDDEN),
"status": status,
}
@@ -1306,11 +1306,11 @@
ELSE @precludedVerdict
END TvStatusV2,
CASE
- WHEN num_passed > 0 THEN @notOverriden -- Flaky and passed verdicts cannot be exonerated.
- WHEN num_failed = 0 AND num_passed = 0 AND num_skipped > 0 THEN @notOverriden -- Skipped verdicts cannot be exonerated.
+ WHEN num_passed > 0 THEN @notOverridden -- Flaky and passed verdicts cannot be exonerated.
+ WHEN num_failed = 0 AND num_passed = 0 AND num_skipped > 0 THEN @notOverridden -- Skipped verdicts cannot be exonerated.
-- The verdict is eligible for exoneration, i.e. one of failed, execution errored or precluded.
WHEN exonerated.TestId IS NOT NULL THEN @exoneratedOverride
- ELSE @notOverriden
+ ELSE @notOverridden
END TvStatusOverride,
{{if .OrderByStatusV2Effective}}
CASE
diff --git a/resultdb/internal/workunits/read.go b/resultdb/internal/workunits/read.go
index fdab875..75e0cd9 100644
--- a/resultdb/internal/workunits/read.go
+++ b/resultdb/internal/workunits/read.go
@@ -29,6 +29,7 @@
"go.chromium.org/luci/resultdb/internal/instructionutil"
"go.chromium.org/luci/resultdb/internal/invocations/invocationspb"
"go.chromium.org/luci/resultdb/internal/spanutil"
+ "go.chromium.org/luci/resultdb/internal/tracing"
pb "go.chromium.org/luci/resultdb/proto/v1"
)
@@ -37,11 +38,8 @@
// NotFound GRPC code.
// For ptrMap see ReadRow comment in span/util.go.
func readColumns(ctx context.Context, id ID, ptrMap map[string]any) error {
- if id.RootInvocationID == "" {
- return errors.New("rootInvocationID is unspecified")
- }
- if id.WorkUnitID == "" {
- return errors.New("workUnitID is unspecified")
+ if err := validateID(id); err != nil {
+ return err
}
err := spanutil.ReadRow(ctx, "WorkUnits", id.key(), ptrMap)
@@ -58,11 +56,12 @@
}
// ReadRealm reads the realm of the given work unit. If the work unit
-// is not found, returns a NotFound appstatus error. Otherwise returns the internal
-// error.
-func ReadRealm(ctx context.Context, id ID) (string, error) {
- var realm string
- err := readColumns(ctx, id, map[string]any{
+// is not found, returns a NotFound appstatus error.
+func ReadRealm(ctx context.Context, id ID) (realm string, err error) {
+ ctx, ts := tracing.Start(ctx, "resultdb.workunits.ReadRealm")
+ defer func() { tracing.End(ts, err) }()
+
+ err = readColumns(ctx, id, map[string]any{
"Realm": &realm,
})
if err != nil {
@@ -71,8 +70,93 @@
return realm, nil
}
-// readMulti reads multiple work units from Spanner.
-func readMulti(ctx context.Context, ids []ID, f func(wu *WorkUnitRow) error) error {
+func validateID(id ID) error {
+ if id.RootInvocationID == "" {
+ return errors.New("rootInvocationID: unspecified")
+ }
+ if id.WorkUnitID == "" {
+ return errors.New("workUnitID: unspecified")
+ }
+ return nil
+}
+
+func validateIDs(ids []ID) error {
+ for i, id := range ids {
+ if err := validateID(id); err != nil {
+ return errors.Fmt("ids[%d]: %w", i, err)
+ }
+ }
+ return nil
+}
+
+// ReadRealms reads the realm of the given work units. If any of the work
+// units are not found, returns a NotFound appstatus error. Returned realms
+// match 1:1 with the requested ids, i.e. result[i] corresponds to ids[i].
+// Duplicate IDs are allowed.
+func ReadRealms(ctx context.Context, ids []ID) (realms []string, err error) {
+ ctx, ts := tracing.Start(ctx, "resultdb.workunits.ReadRealms")
+ defer func() { tracing.End(ts, err) }()
+
+ if err := validateIDs(ids); err != nil {
+ return nil, err
+ }
+ if len(ids) == 0 {
+ return nil, nil
+ }
+
+ realms = make([]string, len(ids))
+
+ // No need to dedup keys going into Spanner, Cloud Spanner always behaves
+ // as if the key is only specified once.
+ var keys []spanner.Key
+ for _, id := range ids {
+ keys = append(keys, id.key())
+ }
+
+ resultMap := make(map[ID]string, len(ids))
+
+ var b spanutil.Buffer
+ columns := []string{"RootInvocationShardId", "WorkUnitId", "Realm"}
+ err = span.Read(ctx, "WorkUnits", spanner.KeySetFromKeys(keys...), columns).Do(func(r *spanner.Row) error {
+ var rootInvocationShardID string
+ var workUnitID string
+ var realm string
+ err := b.FromSpanner(r, &rootInvocationShardID, &workUnitID, &realm)
+ if err != nil {
+ return errors.Fmt("read spanner row for work unit: %w", err)
+ }
+ id := IDFromRowID(rootInvocationShardID, workUnitID)
+ resultMap[id] = realm
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ for i, id := range ids {
+ realm, ok := resultMap[id]
+ if !ok {
+ return nil, appstatus.Errorf(codes.NotFound, "%s not found", id.Name())
+ }
+ realms[i] = realm
+ }
+ return realms, nil
+}
+
+// ReadMask controls what fields to read.
+type ReadMask int
+
+const (
+ // Read all work unit properties.
+ AllFields ReadMask = iota
+ // Read all work unit fields, except extended properties.
+ // As extended properties can be quite large (megabytes per row), when
+ // reading many rows this can be too much data to query.
+ ExcludeExtendedProperties
+)
+
+// readBatchInternal reads multiple work units from Spanner.
+func readBatchInternal(ctx context.Context, ids []ID, mask ReadMask, f func(wu *WorkUnitRow) error) error {
if len(ids) == 0 {
return nil
}
@@ -94,7 +178,9 @@
"Tags",
"Properties",
"Instructions",
- "ExtendedProperties",
+ }
+ if mask == AllFields {
+ cols = append(cols, "ExtendedProperties")
}
keys := spanner.KeySets()
@@ -131,33 +217,36 @@
&wu.Tags,
&properties,
&instructions,
- &extendedProperties,
+ }
+ if mask == AllFields {
+ dest = append(dest, &extendedProperties)
}
if err := b.FromSpanner(row, dest...); err != nil {
- return err
+ return errors.Fmt("read spanner row for work unit: %w", err)
}
wu.ID = IDFromRowID(rootInvocationShardID, workUnitID)
if len(properties) > 0 {
wu.Properties = &structpb.Struct{}
if err := proto.Unmarshal(properties, wu.Properties); err != nil {
- return err
+ return errors.Fmt("unmarshal properties for work unit %s: %w", wu.ID.Name(), err)
}
}
if len(instructions) > 0 {
wu.Instructions = &pb.Instructions{}
if err := proto.Unmarshal(instructions, wu.Instructions); err != nil {
- return err
+ return errors.Fmt("unmarshal instructions for work unit %s: %w", wu.ID.Name(), err)
}
+ // Populate output-only fields.
wu.Instructions = instructionutil.InstructionsWithNames(wu.Instructions, wu.ID.Name())
}
if len(extendedProperties) > 0 {
internalExtendedProperties := &invocationspb.ExtendedProperties{}
if err := proto.Unmarshal(extendedProperties, internalExtendedProperties); err != nil {
- return errors.Fmt("unmarshal ExtendedProperties for work unit %s: %w", wu.ID.Name(), err)
+ return errors.Fmt("unmarshal extended properties for work unit %s: %w", wu.ID.Name(), err)
}
wu.ExtendedProperties = internalExtendedProperties.ExtendedProperties
}
@@ -169,9 +258,14 @@
// Read reads one work unit from Spanner.
// If the work unit does not exist, the returned error is annotated with
// NotFound GRPC code.
-func Read(ctx context.Context, id ID) (*WorkUnitRow, error) {
- var ret *WorkUnitRow
- err := readMulti(ctx, []ID{id}, func(wu *WorkUnitRow) error {
+func Read(ctx context.Context, id ID, mask ReadMask) (ret *WorkUnitRow, err error) {
+ ctx, ts := tracing.Start(ctx, "resultdb.workunits.Read")
+ defer func() { tracing.End(ts, err) }()
+ if err := validateID(id); err != nil {
+ return nil, err
+ }
+
+ err = readBatchInternal(ctx, []ID{id}, mask, func(wu *WorkUnitRow) error {
ret = wu
return nil
})
@@ -185,3 +279,37 @@
return ret, nil
}
}
+
+// ReadBatch reads the given work units. If any of the work units are not found,
+// returns a NotFound appstatus error. Returned realms match 1:1 with the requested
+// ids, i.e. result[i] corresponds to ids[i].
+// Duplicate IDs are allowed.
+func ReadBatch(ctx context.Context, ids []ID, mask ReadMask) (ret []*WorkUnitRow, err error) {
+ ctx, ts := tracing.Start(ctx, "resultdb.workunits.ReadBatch")
+ defer func() { tracing.End(ts, err) }()
+ if err := validateIDs(ids); err != nil {
+ return nil, err
+ }
+
+ resultMap := make(map[ID]*WorkUnitRow, len(ids))
+ err = readBatchInternal(ctx, ids, mask, func(wu *WorkUnitRow) error {
+ resultMap[wu.ID] = wu
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ ret = make([]*WorkUnitRow, len(ids))
+ for i, id := range ids {
+ row, ok := resultMap[id]
+ if !ok {
+ return nil, appstatus.Errorf(codes.NotFound, "%s not found", id.Name())
+ }
+ // Clone the row to avoid aliasing the same result row object in
+ // result twice (in case of the same ID being requested twice),
+ // as the caller might not expect aliasing.
+ ret[i] = row.Clone()
+ }
+ return ret, err
+}
diff --git a/resultdb/internal/workunits/read_test.go b/resultdb/internal/workunits/read_test.go
index e96bb9f..b2cf207 100644
--- a/resultdb/internal/workunits/read_test.go
+++ b/resultdb/internal/workunits/read_test.go
@@ -25,7 +25,6 @@
"go.chromium.org/luci/grpc/appstatus"
"go.chromium.org/luci/server/span"
- "go.chromium.org/luci/resultdb/internal/instructionutil"
"go.chromium.org/luci/resultdb/internal/rootinvocations"
"go.chromium.org/luci/resultdb/internal/testutil"
)
@@ -34,32 +33,50 @@
ftt.Run("Read functions", t, func(t *ftt.Test) {
ctx := testutil.SpannerTestContext(t)
- const realm = "testproject:testrealm"
rootInvID := rootinvocations.ID("root-inv-id")
+
+ // Insert a root invocation.
+ rootInv := rootinvocations.NewBuilder(rootInvID).WithRealm("testproject:root").Build()
+ ms := rootinvocations.InsertForTesting(rootInv)
+
+ // Insert a work unit with all fields set.
+ testData := NewBuilder(rootInvID, "work-unit-id").WithRealm("testproject:wu1").Build()
+ ms = append(ms, InsertForTesting(testData)...)
id := ID{
RootInvocationID: rootInvID,
WorkUnitID: "work-unit-id",
}
- // Insert a root invocation.
- rootInv := rootinvocations.NewBuilder(rootInvID).WithRealm(realm).Build()
- mutations := rootinvocations.InsertForTesting(rootInv)
+ // Insert a work unit with minimal fields set.
+ testDataMinimal := NewBuilder(rootInvID, "work-unit-id-minimal").WithRealm("testproject:wu1").WithMinimalFields().Build()
+ idMinimal := ID{
+ RootInvocationID: rootInvID,
+ WorkUnitID: "work-unit-id-minimal",
+ }
+ ms = append(ms, InsertForTesting(testDataMinimal)...)
- // Insert a work unit.
- testData := NewBuilder(rootInvID, "work-unit-id").WithRealm(realm).Build()
- mutations = append(mutations, InsertForTesting(testData)...)
- testutil.MustApply(ctx, t, mutations...)
+ testutil.MustApply(ctx, t, ms...)
t.Run("Read", func(t *ftt.Test) {
t.Run("happy path", func(t *ftt.Test) {
- row, err := Read(span.Single(ctx), id)
- assert.Loosely(t, err, should.BeNil)
+ t.Run("mask: all fields", func(t *ftt.Test) {
+ row, err := Read(span.Single(ctx), id, AllFields)
+ assert.Loosely(t, err, should.BeNil)
+ assert.That(t, row, should.Match(testData))
+ })
+ t.Run("mask: exclude extended properties", func(t *ftt.Test) {
+ assert.Loosely(t, testData.ExtendedProperties, should.NotBeNil)
+ testData.ExtendedProperties = nil
- // Read populates some fields that the builder doesn't.
- expected := testData
- expected.Instructions = instructionutil.InstructionsWithNames(expected.Instructions, id.Name())
-
- assert.That(t, row, should.Match(&expected))
+ row, err := Read(span.Single(ctx), id, ExcludeExtendedProperties)
+ assert.Loosely(t, err, should.BeNil)
+ assert.That(t, row, should.Match(testData))
+ })
+ t.Run("row: minimal fields", func(t *ftt.Test) {
+ row, err := Read(span.Single(ctx), idMinimal, AllFields)
+ assert.Loosely(t, err, should.BeNil)
+ assert.That(t, row, should.Match(testDataMinimal))
+ })
})
t.Run("not found", func(t *ftt.Test) {
@@ -67,17 +84,103 @@
RootInvocationID: rootInvID,
WorkUnitID: "non-existent-id",
}
- _, err := Read(span.Single(ctx), nonExistentID)
+ _, err := Read(span.Single(ctx), nonExistentID, AllFields)
assert.That(t, appstatus.Code(err), should.Equal(codes.NotFound))
assert.That(t, err, should.ErrLike("rootInvocations/root-inv-id/workUnits/non-existent-id not found"))
})
+ t.Run("empty root invocation ID", func(t *ftt.Test) {
+ id.RootInvocationID = ""
+ _, err := Read(span.Single(ctx), id, AllFields)
+ assert.That(t, err, should.ErrLike("rootInvocationID: unspecified"))
+ })
+ t.Run("empty work unit ID", func(t *ftt.Test) {
+ id.WorkUnitID = ""
+ _, err := Read(span.Single(ctx), id, AllFields)
+ assert.That(t, err, should.ErrLike("workUnitID: unspecified"))
+ })
+ })
+ t.Run("ReadBatch", func(t *ftt.Test) {
+ // Insert additional root invocation and work units.
+ rootInv2 := rootinvocations.NewBuilder("root-inv-id2").WithRealm("testproject:root2").Build()
+ wu21 := NewBuilder("root-inv-id2", "work-unit-id").WithRealm("testproject:wu21").Build()
+ wu22 := NewBuilder("root-inv-id2", "work-unit-id2").WithRealm("testproject:wu22").Build()
+ ms := rootinvocations.InsertForTesting(rootInv2)
+ ms = append(ms, InsertForTesting(wu21)...)
+ ms = append(ms, InsertForTesting(wu22)...)
+ testutil.MustApply(ctx, t, ms...)
+
+ t.Run("happy path", func(t *ftt.Test) {
+ ids := []ID{
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id-minimal"},
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id"},
+ {RootInvocationID: "root-inv-id2", WorkUnitID: "work-unit-id2"},
+ {RootInvocationID: "root-inv-id2", WorkUnitID: "work-unit-id"},
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id"}, // Duplicates are allowed.
+ }
+ t.Run("all fields", func(t *ftt.Test) {
+ rows, err := ReadBatch(span.Single(ctx), ids, AllFields)
+ assert.Loosely(t, err, should.BeNil)
+
+ // Check that the returned rows match the expected data.
+ assert.That(t, rows, should.Match([]*WorkUnitRow{
+ testDataMinimal,
+ testData,
+ wu22,
+ wu21,
+ testData,
+ }))
+ })
+ t.Run("exclude extended properties", func(t *ftt.Test) {
+ rows, err := ReadBatch(span.Single(ctx), ids, ExcludeExtendedProperties)
+ assert.Loosely(t, err, should.BeNil)
+
+ // Check that the returned rows match the inserted rows,
+ // minus extended properties.
+ expectedRows := []*WorkUnitRow{
+ testDataMinimal.Clone(),
+ testData.Clone(),
+ wu22.Clone(),
+ wu21.Clone(),
+ testData.Clone(),
+ }
+ for _, r := range expectedRows {
+ r.ExtendedProperties = nil
+ }
+ assert.That(t, rows, should.Match(expectedRows))
+ })
+ })
+ t.Run("not found", func(t *ftt.Test) {
+ ids := []ID{
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id"},
+ {RootInvocationID: rootInvID, WorkUnitID: "non-existent-id"},
+ }
+ _, err := ReadBatch(span.Single(ctx), ids, AllFields)
+ assert.That(t, appstatus.Code(err), should.Equal(codes.NotFound))
+ assert.That(t, err, should.ErrLike("rootInvocations/root-inv-id/workUnits/non-existent-id not found"))
+ })
+ t.Run("empty root invocation ID", func(t *ftt.Test) {
+ ids := []ID{
+ id,
+ {WorkUnitID: "work-unit-id2"},
+ }
+ _, err := ReadBatch(span.Single(ctx), ids, AllFields)
+ assert.That(t, err, should.ErrLike("ids[1]: rootInvocationID: unspecified"))
+ })
+ t.Run("empty work unit ID", func(t *ftt.Test) {
+ ids := []ID{
+ id,
+ {RootInvocationID: rootInvID},
+ }
+ _, err := ReadBatch(span.Single(ctx), ids, AllFields)
+ assert.That(t, err, should.ErrLike("ids[1]: workUnitID: unspecified"))
+ })
})
t.Run("ReadRealm", func(t *ftt.Test) {
t.Run("happy path", func(t *ftt.Test) {
r, err := ReadRealm(span.Single(ctx), id)
assert.Loosely(t, err, should.BeNil)
- assert.That(t, r, should.Equal(realm))
+ assert.That(t, r, should.Equal("testproject:wu1"))
})
t.Run("not found", func(t *ftt.Test) {
@@ -94,12 +197,76 @@
t.Run("empty root invocation ID", func(t *ftt.Test) {
_, err := ReadRealm(span.Single(ctx), ID{WorkUnitID: "work-unit-id"})
- assert.That(t, err, should.ErrLike("rootInvocationID is unspecified"))
+ assert.That(t, err, should.ErrLike("rootInvocationID: unspecified"))
})
t.Run("empty work unit ID", func(t *ftt.Test) {
_, err := ReadRealm(span.Single(ctx), ID{RootInvocationID: rootInvID})
- assert.That(t, err, should.ErrLike("workUnitID is unspecified"))
+ assert.That(t, err, should.ErrLike("workUnitID: unspecified"))
+ })
+ })
+ t.Run("ReadRealms", func(t *ftt.Test) {
+ // Insert an additional work unit in the existing root invocation.
+ wu2 := NewBuilder(rootInvID, "work-unit-id2").WithRealm("testproject:wu2").Build()
+ ms := InsertForTesting(wu2)
+
+ // Create a further root invocation with work units.
+ rootInv2 := rootinvocations.NewBuilder("root-inv-id2").WithRealm("testproject:root2").Build()
+ wu21 := NewBuilder("root-inv-id2", "work-unit-id").WithRealm("testproject:wu21").Build()
+ wu22 := NewBuilder("root-inv-id2", "work-unit-id2").WithRealm("testproject:wu22").Build()
+ ms = append(ms, rootinvocations.InsertForTesting(rootInv2)...)
+ ms = append(ms, InsertForTesting(wu21)...)
+ ms = append(ms, InsertForTesting(wu22)...)
+
+ testutil.MustApply(ctx, t, ms...)
+
+ t.Run("happy path", func(t *ftt.Test) {
+ ids := []ID{
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id"},
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id2"},
+ {RootInvocationID: "root-inv-id2", WorkUnitID: "work-unit-id2"},
+ {RootInvocationID: "root-inv-id2", WorkUnitID: "work-unit-id"},
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id2"}, // Duplicates are allowed.
+ }
+ realms, err := ReadRealms(span.Single(ctx), ids)
+ assert.Loosely(t, err, should.BeNil)
+ assert.That(t, realms, should.Match([]string{
+ "testproject:wu1",
+ "testproject:wu2",
+ "testproject:wu22",
+ "testproject:wu21",
+ "testproject:wu2",
+ }))
+ })
+
+ t.Run("not found", func(t *ftt.Test) {
+ ids := []ID{
+ {RootInvocationID: rootInvID, WorkUnitID: "work-unit-id"},
+ {RootInvocationID: rootInvID, WorkUnitID: "non-existent-id"},
+ }
+ _, err := ReadRealms(span.Single(ctx), ids)
+ st, ok := appstatus.Get(err)
+ assert.Loosely(t, ok, should.BeTrue)
+ assert.Loosely(t, st.Code(), should.Equal(codes.NotFound))
+ assert.Loosely(t, st.Message(), should.ContainSubstring("rootInvocations/root-inv-id/workUnits/non-existent-id not found"))
+ })
+
+ t.Run("empty root invocation ID", func(t *ftt.Test) {
+ ids := []ID{
+ id,
+ {WorkUnitID: "work-unit-id2"},
+ }
+ _, err := ReadRealms(span.Single(ctx), ids)
+ assert.That(t, err, should.ErrLike("ids[1]: rootInvocationID: unspecified"))
+ })
+
+ t.Run("empty work unit ID", func(t *ftt.Test) {
+ ids := []ID{
+ id,
+ {RootInvocationID: rootInvID},
+ }
+ _, err := ReadRealms(span.Single(ctx), ids)
+ assert.That(t, err, should.ErrLike("ids[1]: workUnitID: unspecified"))
})
})
})
diff --git a/resultdb/internal/workunits/span.go b/resultdb/internal/workunits/span.go
index 2bb53ab..29595ba 100644
--- a/resultdb/internal/workunits/span.go
+++ b/resultdb/internal/workunits/span.go
@@ -19,8 +19,10 @@
"time"
"cloud.google.com/go/spanner"
+ "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
+ "go.chromium.org/luci/resultdb/internal/instructionutil"
"go.chromium.org/luci/resultdb/internal/invocations"
"go.chromium.org/luci/resultdb/internal/invocations/invocationspb"
"go.chromium.org/luci/resultdb/internal/spanutil"
@@ -94,9 +96,31 @@
ExtendedProperties map[string]*structpb.Struct
}
-func (w *WorkUnitRow) toMutation() *spanner.Mutation {
- // TODO: Normalise instruction names before writing.
+// Clone makes a deep copy of the row.
+func (w *WorkUnitRow) Clone() *WorkUnitRow {
+ ret := *w
+ if w.Tags != nil {
+ ret.Tags = make([]*pb.StringPair, len(w.Tags))
+ for i, tp := range w.Tags {
+ ret.Tags[i] = proto.Clone(tp).(*pb.StringPair)
+ }
+ }
+ if w.Properties != nil {
+ ret.Properties = proto.Clone(w.Properties).(*structpb.Struct)
+ }
+ if w.Instructions != nil {
+ ret.Instructions = proto.Clone(w.Instructions).(*pb.Instructions)
+ }
+ if w.ExtendedProperties != nil {
+ ret.ExtendedProperties = make(map[string]*structpb.Struct, len(w.ExtendedProperties))
+ for k, v := range w.ExtendedProperties {
+ ret.ExtendedProperties[k] = proto.Clone(v).(*structpb.Struct)
+ }
+ }
+ return &ret
+}
+func (w *WorkUnitRow) toMutation() *spanner.Mutation {
row := map[string]interface{}{
"RootInvocationShardId": w.ID.RootInvocationShardID(),
"WorkUnitId": w.ID.WorkUnitID,
@@ -111,7 +135,7 @@
"ProducerResource": w.ProducerResource,
"Tags": w.Tags,
"Properties": spanutil.Compressed(pbutil.MustMarshal(w.Properties)),
- "Instructions": spanutil.Compressed(pbutil.MustMarshal(w.Instructions)),
+ "Instructions": spanutil.Compressed(pbutil.MustMarshal(instructionutil.RemoveInstructionsName(w.Instructions))),
}
// Wrap into luci.resultdb.internal.invocations.ExtendedProperties so that
// it can be serialized as a single value to spanner.
@@ -127,8 +151,6 @@
}
func (w *WorkUnitRow) toLegacyInvocationMutation(opts LegacyCreateOptions) *spanner.Mutation {
- // TODO: Normalise instruction names before writing.
-
row := map[string]interface{}{
"InvocationId": w.ID.LegacyInvocationID(),
"Type": invocations.WorkUnit,
@@ -148,7 +170,7 @@
"InheritSources": spanner.NullBool{Valid: false},
// Work units are not export roots.
"IsExportRoot": spanner.NullBool{Bool: false, Valid: true},
- "Instructions": spanutil.Compressed(pbutil.MustMarshal(w.Instructions)),
+ "Instructions": spanutil.Compressed(pbutil.MustMarshal(instructionutil.RemoveInstructionsName(w.Instructions))),
}
// Wrap into luci.resultdb.internal.invocations.ExtendedProperties so that
diff --git a/resultdb/internal/workunits/span_test.go b/resultdb/internal/workunits/span_test.go
index 5abb7b8..65dd960 100644
--- a/resultdb/internal/workunits/span_test.go
+++ b/resultdb/internal/workunits/span_test.go
@@ -20,7 +20,6 @@
"time"
"cloud.google.com/go/spanner"
- "google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/clock/testclock"
@@ -42,53 +41,11 @@
now := testclock.TestRecentTimeUTC
ctx, _ = testclock.UseTime(ctx, now)
- properties := &structpb.Struct{
- Fields: map[string]*structpb.Value{
- "key": structpb.NewStringValue("value"),
- },
- }
- instructions := &pb.Instructions{
- Instructions: []*pb.Instruction{
- {
- Id: "step",
- Type: pb.InstructionType_STEP_INSTRUCTION,
- TargetedInstructions: []*pb.TargetedInstruction{
- {
- Targets: []pb.InstructionTarget{
- pb.InstructionTarget_LOCAL,
- pb.InstructionTarget_REMOTE,
- },
- Content: "step instruction",
- },
- },
- },
- },
- }
- extendedProperties := map[string]*structpb.Struct{
- "mykey": {
- Fields: map[string]*structpb.Value{
- "@type": structpb.NewStringValue("foo.bar.com/x/_some.package.MyMessage"),
- "child_key_1": structpb.NewStringValue("child_value_1"),
- },
- },
- }
id := ID{
RootInvocationID: "root-inv-id",
WorkUnitID: "work-unit-id",
}
- row := &WorkUnitRow{
- ID: id,
- State: pb.WorkUnit_ACTIVE,
- Realm: "testproject:testrealm",
- CreatedBy: "user:test@example.com",
- Deadline: now.Add(2 * 24 * time.Hour),
- CreateRequestID: "test-request-id",
- ProducerResource: "//builds.example.com/builds/123",
- Tags: pbutil.StringPairs("k2", "v2", "k1", "v1"),
- Properties: properties,
- Instructions: instructions,
- ExtendedProperties: extendedProperties,
- }
+ row := NewBuilder("root-inv-id", "work-unit-id").WithState(pb.WorkUnit_ACTIVE).Build()
LegacyCreateOptions := LegacyCreateOptions{
ExpectedTestResultsExpirationTime: now.Add(2 * 24 * time.Hour),
@@ -100,7 +57,7 @@
rootinvocations.InsertForTesting(rootinvocations.NewBuilder("root-inv-id").Build())...,
)
commitTime, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
- mutations := Create(row, LegacyCreateOptions)
+ mutations := Create(row.Clone(), LegacyCreateOptions)
span.BufferWrite(ctx, mutations...)
return nil
})
@@ -111,11 +68,10 @@
defer cancel()
// Validate WorkUnits table entry.
- readWorkUnit, err := Read(ctx, id)
+ readWorkUnit, err := Read(ctx, id, AllFields)
assert.Loosely(t, err, should.BeNil)
- row.CreateTime = commitTime
+ row.CreateTime = commitTime.In(time.UTC)
row.SecondaryIndexShardID = id.shardID(secondaryIndexShardCount)
- row.Instructions.Instructions[0].Name = "rootInvocations/root-inv-id/workUnits/work-unit-id/instructions/step"
assert.Loosely(t, readWorkUnit, should.Match(row))
// Validate Legacy Invocations table entry.
diff --git a/resultdb/internal/workunits/testdata.go b/resultdb/internal/workunits/testdata.go
index f57601c..8e3d923 100644
--- a/resultdb/internal/workunits/testdata.go
+++ b/resultdb/internal/workunits/testdata.go
@@ -18,9 +18,9 @@
"time"
"cloud.google.com/go/spanner"
- "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
+ "go.chromium.org/luci/resultdb/internal/instructionutil"
"go.chromium.org/luci/resultdb/internal/invocations"
"go.chromium.org/luci/resultdb/internal/invocations/invocationspb"
"go.chromium.org/luci/resultdb/internal/rootinvocations"
@@ -39,6 +39,8 @@
func NewBuilder(rootInvocationID rootinvocations.ID, workUnitID string) *Builder {
id := ID{RootInvocationID: rootInvocationID, WorkUnitID: workUnitID}
return &Builder{
+ // Default to populating all fields (except parent work unit ID as it is not known),
+ // this helps maximise test coverage.
row: WorkUnitRow{
ID: id,
SecondaryIndexShardID: id.shardID(secondaryIndexShardCount),
@@ -77,6 +79,30 @@
}
}
+// WithMinimalFields clears as many fields as possible on the work unit while keeping
+// it a valid work unit. This helps test code handles unset fields.
+func (b *Builder) WithMinimalFields() *Builder {
+ b.row = WorkUnitRow{
+ ID: b.row.ID,
+ ParentWorkUnitID: b.row.ParentWorkUnitID,
+ SecondaryIndexShardID: b.row.SecondaryIndexShardID,
+ // Means the finalized time and start time will be cleared in Build() unless state is
+ // subsequently overridden.
+ State: pb.WorkUnit_ACTIVE,
+ Realm: b.row.Realm,
+ CreateTime: b.row.CreateTime,
+ CreatedBy: b.row.CreatedBy,
+ FinalizeStartTime: b.row.FinalizeStartTime,
+ FinalizeTime: b.row.FinalizeTime,
+ Deadline: b.row.Deadline,
+ CreateRequestID: b.row.CreateRequestID,
+ // Prefer to use empty slice rather than nil (even though semantically identical)
+ // as this what we always report in reads.
+ Tags: []*pb.StringPair{},
+ }
+ return b
+}
+
// WithID sets the work unit ID.
func (b *Builder) WithID(id ID) *Builder {
b.row.ID = id
@@ -169,25 +195,14 @@
}
// Build returns the constructed WorkUnitRow.
-func (b *Builder) Build() WorkUnitRow {
- // Return a copy.
- r := b.row
- if r.Tags != nil {
- r.Tags = append([]*pb.StringPair(nil), r.Tags...)
- }
- if r.Properties != nil {
- r.Properties = proto.Clone(r.Properties).(*structpb.Struct)
- }
- if r.Instructions != nil {
- r.Instructions = proto.Clone(r.Instructions).(*pb.Instructions)
- }
- if r.ExtendedProperties != nil {
- newEP := make(map[string]*structpb.Struct, len(r.ExtendedProperties))
- for k, v := range r.ExtendedProperties {
- newEP[k] = proto.Clone(v).(*structpb.Struct)
- }
- r.ExtendedProperties = newEP
- }
+func (b *Builder) Build() *WorkUnitRow {
+ // Return a copy to avoid changes to the returned object
+ // flowing back into the builder.
+ r := b.row.Clone()
+
+ // Populate output-only fields on instructions.
+ r.Instructions = instructionutil.InstructionsWithNames(r.Instructions, r.ID.Name())
+
if r.State == pb.WorkUnit_ACTIVE {
r.FinalizeStartTime = spanner.NullTime{}
r.FinalizeTime = spanner.NullTime{}
@@ -200,7 +215,7 @@
// InsertForTesting inserts the work unit record and its corresponding
// legacy invocation record for testing purposes.
-func InsertForTesting(w WorkUnitRow) []*spanner.Mutation {
+func InsertForTesting(w *WorkUnitRow) []*spanner.Mutation {
workUnitMutation := spanutil.InsertMap("WorkUnits", map[string]any{
"RootInvocationShardId": w.ID.RootInvocationShardID(),
"WorkUnitId": w.ID.WorkUnitID,
@@ -217,7 +232,7 @@
"ProducerResource": w.ProducerResource,
"Tags": w.Tags,
"Properties": spanutil.Compressed(pbutil.MustMarshal(w.Properties)),
- "Instructions": spanutil.Compressed(pbutil.MustMarshal(w.Instructions)),
+ "Instructions": spanutil.Compressed(pbutil.MustMarshal(instructionutil.RemoveInstructionsName(w.Instructions))),
"ExtendedProperties": spanutil.Compressed(pbutil.MustMarshal(&invocationspb.ExtendedProperties{ExtendedProperties: w.ExtendedProperties})),
})
@@ -240,7 +255,7 @@
"Properties": spanutil.Compressed(pbutil.MustMarshal(w.Properties)),
"InheritSources": spanner.NullBool{Valid: false},
"IsExportRoot": spanner.NullBool{Bool: false, Valid: true},
- "Instructions": spanutil.Compressed(pbutil.MustMarshal(w.Instructions)),
+ "Instructions": spanutil.Compressed(pbutil.MustMarshal(instructionutil.RemoveInstructionsName(w.Instructions))),
"ExtendedProperties": spanutil.Compressed(pbutil.MustMarshal(&invocationspb.ExtendedProperties{ExtendedProperties: w.ExtendedProperties})),
})