From a9f671c62f90c5b84e1267b89e89473fe3309264 Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Wed, 28 Oct 2026 16:28:35 +0000 Subject: [PATCH] feat(stovepipe): Add project outcome counts --- api/stovepipe/proto/stovepipe.proto | 4 + api/stovepipe/protopb/stovepipe.pb.go | 29 ++- api/stovepipe/protopb/stovepipe.pb.yarpc.go | 174 +++++++++--------- doc/rfc/stovepipe/list-api.md | 8 +- doc/rfc/stovepipe/request-history-api.md | 2 +- doc/rfc/stovepipe/request-log.md | 2 +- doc/rfc/stovepipe/steps/record.md | 2 +- service/stovepipe/server/mapper/list.go | 18 +- service/stovepipe/server/mapper/list_test.go | 19 ++ .../server/mapper/request_history_test.go | 2 + stovepipe/controller/record/record.go | 13 +- stovepipe/controller/record/record_test.go | 44 ++++- stovepipe/core/requestlog/materializer.go | 2 + stovepipe/core/requestlog/request_summary.go | 13 ++ .../core/requestlog/request_summary_test.go | 89 ++++++++- stovepipe/entity/request_summary.go | 4 + .../storage/mysql/request_summary_store.go | 14 +- .../mysql/request_summary_store_test.go | 12 +- .../storage/mysql/schema/request_summary.sql | 2 + .../extension/storage/mysql/storage_test.go | 3 + test/integration/stovepipe/suite_test.go | 2 +- .../gen/api/stovepipe/proto/stovepipe_pb.ts | 16 +- 22 files changed, 348 insertions(+), 126 deletions(-) diff --git a/api/stovepipe/proto/stovepipe.proto b/api/stovepipe/proto/stovepipe.proto index 772b5b954..cbef6506a 100644 --- a/api/stovepipe/proto/stovepipe.proto +++ b/api/stovepipe/proto/stovepipe.proto @@ -223,6 +223,10 @@ message RequestSummary { int64 accepted_at_ms = 7; // Stable reason for the represented state. Empty when unavailable or inapplicable. string outcome_reason = 8; + // Total recorded project results; zero when no project results have been recorded. + int32 project_count = 9; + // Recorded project results with degree above zero. + int32 broken_project_count = 10; } // ListRequest selects a page of retained request summaries for one queue. diff --git a/api/stovepipe/protopb/stovepipe.pb.go b/api/stovepipe/protopb/stovepipe.pb.go index 2c7f6161a..f5782a57a 100644 --- a/api/stovepipe/protopb/stovepipe.pb.go +++ b/api/stovepipe/protopb/stovepipe.pb.go @@ -1271,8 +1271,12 @@ type RequestSummary struct { AcceptedAtMs int64 `protobuf:"varint,7,opt,name=accepted_at_ms,json=acceptedAtMs,proto3" json:"accepted_at_ms,omitempty"` // Stable reason for the represented state. Empty when unavailable or inapplicable. OutcomeReason string `protobuf:"bytes,8,opt,name=outcome_reason,json=outcomeReason,proto3" json:"outcome_reason,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Total recorded project results; zero when no project results have been recorded. + ProjectCount int32 `protobuf:"varint,9,opt,name=project_count,json=projectCount,proto3" json:"project_count,omitempty"` + // Recorded project results with degree above zero. + BrokenProjectCount int32 `protobuf:"varint,10,opt,name=broken_project_count,json=brokenProjectCount,proto3" json:"broken_project_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RequestSummary) Reset() { @@ -1361,6 +1365,20 @@ func (x *RequestSummary) GetOutcomeReason() string { return "" } +func (x *RequestSummary) GetProjectCount() int32 { + if x != nil { + return x.ProjectCount + } + return 0 +} + +func (x *RequestSummary) GetBrokenProjectCount() int32 { + if x != nil { + return x.BrokenProjectCount + } + return 0 +} + // ListRequest selects a page of retained request summaries for one queue. type ListRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1820,7 +1838,7 @@ const file_stovepipe_proto_rawDesc = "" + "\x18project_results_complete\x18\a \x01(\bR\x16projectResultsComplete\x12I\n" + "\bprojects\x18\b \x03(\v2-.uber.submitqueue.stovepipe.ProjectValidationR\bprojects\x12&\n" + "\x0fnext_page_token\x18\t \x01(\tR\rnextPageTokenB\x13\n" + - "\x11repository_result\"\xa0\x02\n" + + "\x11repository_result\"\xf7\x02\n" + "\x0eRequestSummary\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x14\n" + @@ -1831,7 +1849,10 @@ const file_stovepipe_proto_rawDesc = "" + "\rrequest_state\x18\x05 \x01(\tR\frequestState\x12-\n" + "\x13state_updated_at_ms\x18\x06 \x01(\x03R\x10stateUpdatedAtMs\x12$\n" + "\x0eaccepted_at_ms\x18\a \x01(\x03R\facceptedAtMs\x12%\n" + - "\x0eoutcome_reason\x18\b \x01(\tR\routcomeReason\"\xf7\x01\n" + + "\x0eoutcome_reason\x18\b \x01(\tR\routcomeReason\x12#\n" + + "\rproject_count\x18\t \x01(\x05R\fprojectCount\x120\n" + + "\x14broken_project_count\x18\n" + + " \x01(\x05R\x12brokenProjectCount\"\xf7\x01\n" + "\vListRequest\x12\x14\n" + "\x05queue\x18\x01 \x01(\tR\x05queue\x126\n" + "\x17accepted_at_or_after_ms\x18\x02 \x01(\x03H\x00R\x13acceptedAtOrAfterMs\x12.\n" + diff --git a/api/stovepipe/protopb/stovepipe.pb.yarpc.go b/api/stovepipe/protopb/stovepipe.pb.yarpc.go index b67fd871b..c63656e3d 100644 --- a/api/stovepipe/protopb/stovepipe.pb.yarpc.go +++ b/api/stovepipe/protopb/stovepipe.pb.yarpc.go @@ -576,92 +576,94 @@ var ( var yarpcFileDescriptorClosurefabdb6b3c0b09022 = [][]byte{ // stovepipe.proto []byte{ - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x18, 0x4b, 0x6f, 0x1b, 0x55, - 0x37, 0x63, 0xe7, 0x61, 0x9f, 0x24, 0x4e, 0x7a, 0xdb, 0xe6, 0x73, 0xdc, 0xa6, 0xca, 0x37, 0x50, - 0x9a, 0x16, 0xd5, 0x51, 0x83, 0x04, 0x2d, 0x0b, 0x44, 0xdc, 0xb8, 0xb1, 0xd5, 0x26, 0x4d, 0xc7, - 0x0d, 0x48, 0x74, 0x31, 0x1a, 0xdb, 0x27, 0xce, 0xb4, 0x99, 0x47, 0xef, 0xbd, 0x13, 0x48, 0x17, - 0x48, 0x2c, 0x58, 0x21, 0x21, 0xf1, 0x0f, 0x58, 0xc3, 0x9e, 0x35, 0x2b, 0xfe, 0x00, 0x1b, 0xf8, - 0x05, 0xfc, 0x04, 0xb6, 0xe8, 0x3e, 0x66, 0x3c, 0x7e, 0xc4, 0x69, 0x92, 0x22, 0x81, 0xc4, 0x6e, - 0xce, 0xfb, 0xdc, 0xf3, 0xba, 0xe7, 0x0e, 0xcc, 0x31, 0x1e, 0x1c, 0x62, 0xe8, 0x86, 0x58, 0x0e, - 0x69, 0xc0, 0x03, 0x52, 0x8a, 0x9a, 0x48, 0xcb, 0x2c, 0x6a, 0x7a, 0x2e, 0x7f, 0x19, 0x61, 0x84, - 0xe5, 0x84, 0xc3, 0xbc, 0x01, 0xd3, 0x3b, 0xae, 0xdf, 0xb1, 0xf0, 0x65, 0x84, 0x8c, 0x93, 0x22, - 0x4c, 0x79, 0xc8, 0x98, 0xd3, 0xc1, 0xa2, 0xb1, 0x6c, 0xac, 0xe4, 0xad, 0x18, 0x34, 0xbf, 0x36, - 0x60, 0x46, 0x71, 0xb2, 0x30, 0xf0, 0x19, 0x1e, 0xcf, 0x4a, 0xfe, 0x0f, 0x33, 0x0c, 0xe9, 0xa1, - 0xdb, 0x42, 0xdb, 0x77, 0x3c, 0x2c, 0x66, 0x24, 0x79, 0x5a, 0xe3, 0xb6, 0x1d, 0x0f, 0xc9, 0x55, - 0xc8, 0x73, 0xd7, 0x43, 0xc6, 0x1d, 0x2f, 0x2c, 0x66, 0x97, 0x8d, 0x95, 0xac, 0xd5, 0x45, 0x90, - 0x12, 0xe4, 0xf6, 0x03, 0xc6, 0xa5, 0xf0, 0xb8, 0x14, 0x4e, 0x60, 0x93, 0xc2, 0x6c, 0xdd, 0xef, - 0x20, 0xe3, 0xb1, 0xcb, 0x97, 0x60, 0x42, 0x1e, 0x4a, 0x7b, 0xa1, 0x00, 0xf2, 0x10, 0x00, 0x7d, - 0xa7, 0x79, 0x80, 0x1e, 0xfa, 0x5c, 0x7a, 0x50, 0x58, 0x7b, 0xb7, 0x7c, 0x7c, 0x20, 0xca, 0x4f, - 0x04, 0x5c, 0x4d, 0x44, 0xac, 0x94, 0xb8, 0xc9, 0xa0, 0x10, 0xdb, 0xd4, 0x87, 0x2f, 0x40, 0xc6, - 0x6d, 0x6b, 0x8b, 0x19, 0xb7, 0x4d, 0x9e, 0x0c, 0x98, 0x9b, 0x5e, 0xbb, 0x73, 0x0a, 0x73, 0x0d, - 0xee, 0xf0, 0x88, 0xf5, 0x18, 0x6d, 0xc0, 0xd5, 0x4d, 0x8c, 0x4f, 0x59, 0x73, 0x19, 0x0f, 0xe8, - 0x51, 0xe5, 0xa8, 0xbe, 0x31, 0xfa, 0xdc, 0x4b, 0x00, 0x54, 0x31, 0xd8, 0x6e, 0x5b, 0x47, 0x3e, - 0xaf, 0x31, 0xf5, 0xb6, 0xb9, 0x09, 0x4b, 0x43, 0x94, 0xee, 0x5a, 0xf5, 0xd1, 0x5a, 0xe7, 0x21, - 0x1b, 0x51, 0x57, 0xab, 0x13, 0x9f, 0xe6, 0xcf, 0x06, 0xcc, 0x68, 0xf9, 0xea, 0x21, 0xfa, 0x9c, - 0x2c, 0x42, 0x0e, 0xc5, 0x87, 0x9d, 0xc4, 0x65, 0x4a, 0xc2, 0xf5, 0xb6, 0xa8, 0x87, 0x24, 0xb7, - 0xb6, 0xc7, 0xa4, 0x9a, 0xac, 0x35, 0x9d, 0xe0, 0xb6, 0x18, 0xb9, 0x0e, 0xb3, 0xb1, 0xdb, 0x8c, - 0x3b, 0x1c, 0x65, 0x4d, 0xe4, 0x6b, 0x63, 0xd6, 0x8c, 0x46, 0x8b, 0x00, 0x21, 0x59, 0x80, 0x09, - 0xa9, 0x54, 0x55, 0x45, 0x6d, 0xcc, 0x52, 0x20, 0xb9, 0x0e, 0x85, 0x20, 0xe2, 0xad, 0xc0, 0x43, - 0x9b, 0xa2, 0xc3, 0x02, 0xbf, 0x38, 0x21, 0x5d, 0x98, 0xd5, 0x58, 0x4b, 0x22, 0x2b, 0x33, 0x00, - 0x41, 0xab, 0x15, 0x51, 0x8a, 0x7e, 0x0b, 0xcd, 0x5f, 0x0c, 0x28, 0xf4, 0x46, 0xa2, 0x2f, 0x7a, - 0x46, 0x5f, 0xf4, 0xc8, 0xc7, 0x30, 0x29, 0xed, 0x89, 0x23, 0x64, 0x57, 0xa6, 0xd7, 0x56, 0x46, - 0x65, 0x38, 0x1d, 0x1d, 0x4b, 0xcb, 0x91, 0x06, 0xcc, 0x25, 0xe7, 0x8c, 0x3c, 0xcf, 0xa1, 0x47, - 0xf2, 0xa4, 0xd3, 0x6b, 0xb7, 0x46, 0xa9, 0xd2, 0x5e, 0x36, 0x94, 0x84, 0x55, 0xa0, 0x3d, 0xb0, - 0xf9, 0x93, 0x31, 0x34, 0xab, 0xa2, 0x54, 0x74, 0xb9, 0x76, 0x1d, 0x37, 0xde, 0x9c, 0xe3, 0x99, - 0x73, 0x3b, 0xfe, 0x1c, 0xae, 0x1d, 0x57, 0x8d, 0xda, 0xf1, 0x1a, 0xe4, 0xf7, 0x25, 0xde, 0xc5, - 0xd8, 0xf7, 0xd7, 0x31, 0xa8, 0x75, 0x59, 0x5d, 0x61, 0xf3, 0x07, 0x03, 0xae, 0x6c, 0x22, 0xdf, - 0xa1, 0xc1, 0x73, 0x6c, 0xe9, 0x7e, 0x3b, 0x6f, 0x3b, 0x89, 0x41, 0x15, 0x2a, 0x85, 0xac, 0x98, - 0x5d, 0xce, 0x8a, 0x41, 0x15, 0xc3, 0xe4, 0x0a, 0xe4, 0x43, 0xa7, 0x83, 0x36, 0x73, 0x5f, 0xa9, - 0x29, 0x36, 0x61, 0xe5, 0x04, 0xa2, 0xe1, 0xbe, 0x92, 0x7a, 0x25, 0x91, 0x07, 0x2f, 0x30, 0x2e, - 0x56, 0xc9, 0xfe, 0x54, 0x20, 0xcc, 0x1f, 0x0d, 0xd9, 0xfc, 0x7d, 0xce, 0x9e, 0xd8, 0xa6, 0x4b, - 0x00, 0xad, 0x7d, 0xc7, 0xef, 0xa0, 0xdd, 0xed, 0xd6, 0xbc, 0xc2, 0xec, 0x52, 0xf7, 0x6f, 0xf3, - 0xb6, 0x09, 0x17, 0xb4, 0xa7, 0x9f, 0x38, 0x07, 0x6e, 0xdb, 0xe1, 0x6e, 0xe0, 0x8b, 0xeb, 0x41, - 0x2b, 0x8f, 0xc7, 0x81, 0x06, 0xc9, 0x4d, 0x98, 0x6b, 0x52, 0x74, 0x5e, 0x08, 0x8d, 0x6d, 0xec, - 0x50, 0x54, 0x37, 0x84, 0x51, 0x1b, 0xb3, 0x0a, 0x31, 0x61, 0x43, 0xe2, 0x2b, 0x39, 0x98, 0xa4, - 0xc8, 0xa2, 0x03, 0x6e, 0xfe, 0x96, 0x1d, 0x16, 0x91, 0x54, 0x89, 0x9f, 0xd0, 0xba, 0x49, 0xc0, - 0x32, 0xc7, 0x07, 0x2c, 0xdb, 0x1f, 0xb0, 0x45, 0xc8, 0x35, 0x1d, 0xa6, 0x88, 0xea, 0x1e, 0x9a, - 0x12, 0xb0, 0x20, 0xbd, 0xd5, 0x3f, 0xb0, 0x54, 0x54, 0x7a, 0xc7, 0x95, 0x09, 0xb3, 0x51, 0xd8, - 0x76, 0x38, 0xb6, 0x6d, 0x87, 0x8b, 0xc9, 0x07, 0x6a, 0xf2, 0x69, 0xe4, 0x3a, 0xdf, 0x62, 0xe4, - 0x23, 0x28, 0x51, 0x0c, 0x03, 0xe6, 0x8a, 0x82, 0xb5, 0xfb, 0x03, 0x33, 0xa9, 0x03, 0x53, 0xec, - 0xf2, 0x54, 0x7a, 0x42, 0x44, 0xee, 0x42, 0x51, 0x07, 0xd6, 0x56, 0xa1, 0x62, 0x76, 0x2b, 0xf0, - 0xc2, 0x03, 0xe4, 0x58, 0x9c, 0x5a, 0x36, 0x56, 0x72, 0xd6, 0x82, 0xa6, 0x5b, 0x8a, 0x7c, 0x5f, - 0x53, 0x49, 0x3d, 0x55, 0x0e, 0x39, 0xd9, 0x5a, 0xb7, 0x47, 0xb5, 0xd6, 0x40, 0x8a, 0x53, 0xd5, - 0xf3, 0x0e, 0xcc, 0xf9, 0xf8, 0x05, 0xb7, 0x53, 0x55, 0x92, 0x57, 0x03, 0x58, 0xa0, 0x77, 0xe2, - 0x4a, 0xa9, 0x5c, 0x84, 0x0b, 0xa9, 0xc3, 0xea, 0xd4, 0xfe, 0x9e, 0x95, 0xe3, 0x6b, 0x58, 0xb1, - 0xff, 0x97, 0xdb, 0x7f, 0x79, 0x6e, 0xbf, 0xcf, 0x24, 0x77, 0xac, 0x1e, 0xfa, 0xff, 0xc8, 0x64, - 0xde, 0x86, 0x8b, 0x92, 0x68, 0xf7, 0xa6, 0x74, 0x52, 0xa6, 0x74, 0x5e, 0x92, 0x76, 0x53, 0x79, - 0x7d, 0x1b, 0x0a, 0x4e, 0xab, 0x85, 0x61, 0x97, 0x73, 0x4a, 0x72, 0xce, 0xc4, 0x58, 0xc9, 0x35, - 0xb8, 0x94, 0xe4, 0x86, 0x2c, 0x25, 0xe6, 0x9f, 0x06, 0x4c, 0x3f, 0x72, 0x4f, 0xda, 0x67, 0xdf, - 0x87, 0xff, 0xa5, 0x4d, 0x06, 0xd4, 0x76, 0xf6, 0x38, 0xd2, 0x64, 0x9d, 0xaa, 0x8d, 0x59, 0x17, - 0xbb, 0xd6, 0x1f, 0xd3, 0x75, 0x41, 0xdd, 0x62, 0xa4, 0x0c, 0x24, 0x91, 0x6b, 0xe2, 0x5e, 0x40, - 0x51, 0x88, 0xc8, 0x8d, 0xbb, 0x66, 0x58, 0xf3, 0x31, 0xad, 0x22, 0x49, 0x5b, 0xe7, 0xba, 0x07, - 0x2a, 0x0b, 0x70, 0x29, 0xb1, 0x75, 0x10, 0x7c, 0x8e, 0xd4, 0x6e, 0x06, 0x91, 0xdf, 0xee, 0xc1, - 0x47, 0x61, 0x18, 0xe3, 0xcd, 0x2f, 0x61, 0x46, 0x1d, 0x5c, 0xb7, 0xf9, 0x03, 0xc8, 0xe9, 0xac, - 0x9c, 0xe6, 0xae, 0x8f, 0x97, 0x8b, 0x44, 0x76, 0x58, 0xc5, 0x66, 0x86, 0x54, 0xac, 0xf9, 0x08, - 0x2e, 0x6f, 0x22, 0x97, 0x9b, 0xb8, 0xde, 0xbf, 0xcf, 0x71, 0xbb, 0x9a, 0xbf, 0x1a, 0x70, 0x79, - 0xe8, 0x56, 0x2f, 0xae, 0xc2, 0x43, 0xa4, 0xcc, 0x0d, 0x7c, 0xa9, 0x30, 0x6b, 0xc5, 0x20, 0x59, - 0x87, 0x09, 0x55, 0x94, 0x67, 0x78, 0xa0, 0x28, 0x49, 0x72, 0x0f, 0x16, 0x71, 0x6f, 0x0f, 0x5b, - 0xdc, 0x3d, 0x44, 0x7b, 0x8f, 0x06, 0x9e, 0x18, 0x11, 0x9e, 0xcb, 0x53, 0x8d, 0xb2, 0x90, 0x30, - 0x3c, 0xa0, 0x81, 0x77, 0x5f, 0x92, 0x45, 0x6b, 0x98, 0x30, 0xab, 0xdc, 0x8f, 0xab, 0x78, 0x5c, - 0x8d, 0x30, 0x8d, 0x14, 0x45, 0x6c, 0x7e, 0x65, 0xc0, 0x42, 0x7f, 0x90, 0x74, 0xba, 0x86, 0x47, - 0xe9, 0xcd, 0xbf, 0x84, 0x6e, 0x1d, 0xc0, 0x5c, 0x1f, 0x13, 0xb9, 0x0a, 0xc5, 0x27, 0xbb, 0xd5, - 0xdd, 0xaa, 0x5d, 0xdd, 0x5e, 0xaf, 0x3c, 0xaa, 0x6e, 0x55, 0xb7, 0x9f, 0xda, 0xbb, 0xdb, 0x0f, - 0xb7, 0x1f, 0x7f, 0xba, 0x3d, 0x3f, 0x36, 0x94, 0xaa, 0x3e, 0x37, 0xe6, 0x0d, 0xb2, 0x04, 0x8b, - 0x03, 0xd4, 0x8d, 0x7a, 0x43, 0x91, 0x33, 0x6b, 0x7f, 0x4c, 0x41, 0xbe, 0x11, 0x7b, 0x47, 0x9e, - 0xc1, 0xb8, 0x78, 0xf5, 0x92, 0x1b, 0x23, 0xc7, 0x67, 0xf7, 0x05, 0x5d, 0x5a, 0x39, 0x99, 0x51, - 0xc5, 0xcf, 0x1c, 0x23, 0x0e, 0x4c, 0xaa, 0x77, 0x25, 0xb9, 0x39, 0x4a, 0xaa, 0xe7, 0xbd, 0x5b, - 0xba, 0xf5, 0x3a, 0xac, 0x89, 0x89, 0x6f, 0x0d, 0x59, 0xe4, 0x83, 0x6f, 0x03, 0x72, 0x77, 0x94, - 0x9e, 0x51, 0x2f, 0xcf, 0xd2, 0xbd, 0x33, 0x48, 0x26, 0x0e, 0x7d, 0xa7, 0x0a, 0x6a, 0xc8, 0xd2, - 0x4f, 0x4e, 0xab, 0xb7, 0xbb, 0x0f, 0x97, 0x3e, 0x3c, 0x8b, 0x68, 0xe2, 0xd3, 0x37, 0x06, 0x5c, - 0x1a, 0xb6, 0x5c, 0x92, 0x0f, 0x4e, 0x50, 0x7b, 0xdc, 0x6b, 0xa2, 0x74, 0xf7, 0xf4, 0x82, 0xfd, - 0x29, 0x1b, 0xdc, 0x87, 0xc8, 0x29, 0xb5, 0xa6, 0xe2, 0x73, 0xef, 0x0c, 0x92, 0x89, 0x43, 0xcf, - 0x60, 0x5c, 0xcc, 0xe9, 0xd1, 0x3d, 0x90, 0xba, 0xc2, 0x46, 0xf7, 0x40, 0x7a, 0xe4, 0x9b, 0x63, - 0xe4, 0x08, 0x0a, 0xbd, 0xf3, 0x85, 0xdc, 0x39, 0xc1, 0xd7, 0xc1, 0x81, 0x5d, 0x5a, 0x3b, 0x8d, - 0x48, 0x6c, 0xba, 0x82, 0x70, 0xad, 0x15, 0x78, 0x23, 0x44, 0x2b, 0x85, 0x64, 0x10, 0xec, 0xd0, - 0x80, 0x07, 0x3b, 0xc6, 0x67, 0x77, 0x3a, 0x2e, 0xdf, 0x8f, 0x9a, 0xe5, 0x56, 0xe0, 0xad, 0x0a, - 0xc1, 0xd5, 0x94, 0xe0, 0xaa, 0x13, 0xba, 0xab, 0x89, 0xf0, 0xaa, 0xfc, 0xf9, 0x16, 0x36, 0x9b, - 0x93, 0xf2, 0xe3, 0xbd, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x68, 0x4f, 0xd2, 0xf5, 0x98, 0x13, - 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x58, 0x4b, 0x6f, 0x1b, 0x55, + 0x14, 0xce, 0xd8, 0x79, 0xd8, 0x27, 0x8e, 0x93, 0xde, 0xa6, 0xc1, 0x71, 0x9b, 0x2a, 0x0c, 0x94, + 0xa6, 0x45, 0x75, 0x68, 0x90, 0xa0, 0x65, 0x81, 0x88, 0x1b, 0x37, 0xb6, 0xda, 0xa4, 0xe9, 0xb8, + 0x01, 0x89, 0x2e, 0x46, 0xe3, 0xf1, 0x89, 0x33, 0x6d, 0xe6, 0xd1, 0x3b, 0x77, 0x02, 0xe9, 0x02, + 0x89, 0x05, 0x2b, 0x24, 0x24, 0xfe, 0x06, 0xec, 0x59, 0xb3, 0xe2, 0x0f, 0xb0, 0x81, 0x5f, 0xc0, + 0x4f, 0xe8, 0x16, 0xdd, 0xc7, 0x8c, 0xc7, 0x8f, 0x38, 0x4d, 0x52, 0x24, 0x90, 0xd8, 0xcd, 0x3d, + 0xaf, 0x7b, 0xee, 0x77, 0x1e, 0xf7, 0xdc, 0x81, 0xd9, 0x90, 0xf9, 0x87, 0x18, 0x38, 0x01, 0x56, + 0x02, 0xea, 0x33, 0x9f, 0x94, 0xa3, 0x16, 0xd2, 0x4a, 0x18, 0xb5, 0x5c, 0x87, 0xbd, 0x88, 0x30, + 0xc2, 0x4a, 0x22, 0xa1, 0x5f, 0x87, 0xe9, 0x1d, 0xc7, 0xeb, 0x18, 0xf8, 0x22, 0xc2, 0x90, 0x91, + 0x12, 0x4c, 0xb9, 0x18, 0x86, 0x56, 0x07, 0x4b, 0xda, 0xb2, 0xb6, 0x92, 0x37, 0xe2, 0xa5, 0xfe, + 0x9d, 0x06, 0x05, 0x29, 0x19, 0x06, 0xbe, 0x17, 0xe2, 0xf1, 0xa2, 0xe4, 0x6d, 0x28, 0x84, 0x48, + 0x0f, 0x1d, 0x1b, 0x4d, 0xcf, 0x72, 0xb1, 0x94, 0x11, 0xec, 0x69, 0x45, 0xdb, 0xb6, 0x5c, 0x24, + 0x57, 0x20, 0xcf, 0x1c, 0x17, 0x43, 0x66, 0xb9, 0x41, 0x29, 0xbb, 0xac, 0xad, 0x64, 0x8d, 0x2e, + 0x81, 0x94, 0x21, 0xb7, 0xef, 0x87, 0x4c, 0x28, 0x8f, 0x0b, 0xe5, 0x64, 0xad, 0x53, 0x98, 0x69, + 0x78, 0x1d, 0x0c, 0x59, 0xec, 0xf2, 0x3c, 0x4c, 0x88, 0x43, 0x29, 0x2f, 0xe4, 0x82, 0x3c, 0x00, + 0x40, 0xcf, 0x6a, 0x1d, 0xa0, 0x8b, 0x1e, 0x13, 0x1e, 0x14, 0xd7, 0xde, 0xaf, 0x1c, 0x0f, 0x44, + 0xe5, 0x31, 0x5f, 0xd7, 0x12, 0x15, 0x23, 0xa5, 0xae, 0x87, 0x50, 0x8c, 0xf7, 0x54, 0x87, 0x2f, + 0x42, 0xc6, 0x69, 0xab, 0x1d, 0x33, 0x4e, 0x9b, 0x3c, 0x1e, 0xd8, 0x6e, 0x7a, 0xed, 0xf6, 0x29, + 0xb6, 0x6b, 0x32, 0x8b, 0x45, 0x61, 0xcf, 0xa6, 0x4d, 0xb8, 0xb2, 0x89, 0xf1, 0x29, 0xeb, 0x4e, + 0xc8, 0x7c, 0x7a, 0x54, 0x3d, 0x6a, 0x6c, 0x8c, 0x3e, 0xf7, 0x12, 0x00, 0x95, 0x02, 0xa6, 0xd3, + 0x56, 0xc8, 0xe7, 0x15, 0xa5, 0xd1, 0xd6, 0x37, 0x61, 0x69, 0x88, 0xd1, 0x5d, 0xa3, 0x31, 0xda, + 0xea, 0x1c, 0x64, 0x23, 0xea, 0x28, 0x73, 0xfc, 0x53, 0xff, 0x55, 0x83, 0x82, 0xd2, 0xaf, 0x1d, + 0xa2, 0xc7, 0xc8, 0x22, 0xe4, 0x90, 0x7f, 0x98, 0x09, 0x2e, 0x53, 0x62, 0xdd, 0x68, 0xf3, 0x7c, + 0x48, 0x62, 0x6b, 0xba, 0xa1, 0x30, 0x93, 0x35, 0xa6, 0x13, 0xda, 0x56, 0x48, 0xae, 0xc1, 0x4c, + 0xec, 0x76, 0xc8, 0x2c, 0x86, 0x22, 0x27, 0xf2, 0xf5, 0x31, 0xa3, 0xa0, 0xc8, 0x1c, 0x20, 0x24, + 0x0b, 0x30, 0x21, 0x8c, 0xca, 0xac, 0xa8, 0x8f, 0x19, 0x72, 0x49, 0xae, 0x41, 0xd1, 0x8f, 0x98, + 0xed, 0xbb, 0x68, 0x52, 0xb4, 0x42, 0xdf, 0x2b, 0x4d, 0x08, 0x17, 0x66, 0x14, 0xd5, 0x10, 0xc4, + 0x6a, 0x01, 0xc0, 0xb7, 0xed, 0x88, 0x52, 0xf4, 0x6c, 0xd4, 0x7f, 0xd3, 0xa0, 0xd8, 0x8b, 0x44, + 0x1f, 0x7a, 0x5a, 0x1f, 0x7a, 0xe4, 0x33, 0x98, 0x14, 0xfb, 0xf1, 0x23, 0x64, 0x57, 0xa6, 0xd7, + 0x56, 0x46, 0x45, 0x38, 0x8d, 0x8e, 0xa1, 0xf4, 0x48, 0x13, 0x66, 0x93, 0x73, 0x46, 0xae, 0x6b, + 0xd1, 0x23, 0x71, 0xd2, 0xe9, 0xb5, 0x9b, 0xa3, 0x4c, 0x29, 0x2f, 0x9b, 0x52, 0xc3, 0x28, 0xd2, + 0x9e, 0xb5, 0xfe, 0x8b, 0x36, 0x34, 0xaa, 0x3c, 0x55, 0x54, 0xba, 0x76, 0x1d, 0xd7, 0xde, 0x9c, + 0xe3, 0x99, 0x73, 0x3b, 0xfe, 0x0c, 0xae, 0x1e, 0x97, 0x8d, 0xca, 0xf1, 0x3a, 0xe4, 0xf7, 0x05, + 0xdd, 0xc1, 0xd8, 0xf7, 0xd7, 0xd9, 0x50, 0xd9, 0x32, 0xba, 0xca, 0xfa, 0x4f, 0x1a, 0x5c, 0xde, + 0x44, 0xb6, 0x43, 0xfd, 0x67, 0x68, 0xab, 0x7a, 0x3b, 0x6f, 0x39, 0xf1, 0x46, 0x15, 0x48, 0x83, + 0x61, 0x29, 0xbb, 0x9c, 0xe5, 0x8d, 0x2a, 0x5e, 0x93, 0xcb, 0x90, 0x0f, 0xac, 0x0e, 0x9a, 0xa1, + 0xf3, 0x52, 0x76, 0xb1, 0x09, 0x23, 0xc7, 0x09, 0x4d, 0xe7, 0xa5, 0xb0, 0x2b, 0x98, 0xcc, 0x7f, + 0x8e, 0x71, 0xb2, 0x0a, 0xf1, 0x27, 0x9c, 0xa0, 0xff, 0xac, 0x89, 0xe2, 0xef, 0x73, 0xf6, 0xc4, + 0x32, 0x5d, 0x02, 0xb0, 0xf7, 0x2d, 0xaf, 0x83, 0x66, 0xb7, 0x5a, 0xf3, 0x92, 0xb2, 0x4b, 0x9d, + 0x7f, 0xcc, 0xdb, 0x16, 0x5c, 0x50, 0x9e, 0x7e, 0x6e, 0x1d, 0x38, 0x6d, 0x8b, 0x39, 0xbe, 0xc7, + 0xaf, 0x07, 0x65, 0x3c, 0x6e, 0x07, 0x6a, 0x49, 0x6e, 0xc0, 0x6c, 0x8b, 0xa2, 0xf5, 0x9c, 0x5b, + 0x6c, 0x63, 0x87, 0xa2, 0xbc, 0x21, 0xb4, 0xfa, 0x98, 0x51, 0x8c, 0x19, 0x1b, 0x82, 0x5e, 0xcd, + 0xc1, 0x24, 0xc5, 0x30, 0x3a, 0x60, 0xfa, 0x1f, 0xd9, 0x61, 0x88, 0xa4, 0x52, 0xfc, 0x84, 0xd2, + 0x4d, 0x00, 0xcb, 0x1c, 0x0f, 0x58, 0xb6, 0x1f, 0xb0, 0x45, 0xc8, 0xb5, 0xac, 0x50, 0x32, 0xe5, + 0x3d, 0x34, 0xc5, 0xd7, 0x9c, 0xf5, 0x4e, 0x7f, 0xc3, 0x92, 0xa8, 0xf4, 0xb6, 0x2b, 0x1d, 0x66, + 0xa2, 0xa0, 0x6d, 0x31, 0x6c, 0x9b, 0x16, 0xe3, 0x9d, 0x0f, 0x64, 0xe7, 0x53, 0xc4, 0x75, 0xb6, + 0x15, 0x92, 0x4f, 0xa1, 0x4c, 0x31, 0xf0, 0x43, 0x87, 0x27, 0xac, 0xd9, 0x0f, 0xcc, 0xa4, 0x02, + 0xa6, 0xd4, 0x95, 0xa9, 0xf6, 0x40, 0x44, 0xee, 0x40, 0x49, 0x01, 0x6b, 0x4a, 0xa8, 0x42, 0xd3, + 0xf6, 0xdd, 0xe0, 0x00, 0x19, 0x96, 0xa6, 0x96, 0xb5, 0x95, 0x9c, 0xb1, 0xa0, 0xf8, 0x86, 0x64, + 0xdf, 0x53, 0x5c, 0xd2, 0x48, 0xa5, 0x43, 0x4e, 0x94, 0xd6, 0xad, 0x51, 0xa5, 0x35, 0x10, 0xe2, + 0x54, 0xf6, 0xbc, 0x07, 0xb3, 0x1e, 0x7e, 0xcd, 0xcc, 0x54, 0x96, 0xe4, 0x65, 0x03, 0xe6, 0xe4, + 0x9d, 0x38, 0x53, 0xaa, 0x17, 0xe1, 0x42, 0xea, 0xb0, 0x2a, 0xb4, 0x7f, 0x66, 0x45, 0xfb, 0x1a, + 0x96, 0xec, 0xff, 0xc7, 0xf6, 0x3f, 0x1e, 0xdb, 0x57, 0x99, 0xe4, 0x8e, 0x55, 0x4d, 0xff, 0x5f, + 0x19, 0xcc, 0x5b, 0x70, 0x51, 0x30, 0xcd, 0xde, 0x90, 0x4e, 0x8a, 0x90, 0xce, 0x09, 0xd6, 0x6e, + 0x2a, 0xae, 0xef, 0x42, 0xd1, 0xb2, 0x6d, 0x0c, 0xba, 0x92, 0x53, 0x42, 0xb2, 0x10, 0x53, 0x85, + 0xd4, 0xe0, 0x50, 0x92, 0x1b, 0x32, 0x94, 0x70, 0x07, 0xe3, 0x20, 0xdb, 0x7e, 0xe4, 0x31, 0x81, + 0xee, 0x84, 0x51, 0x50, 0xc4, 0x7b, 0x9c, 0x46, 0x3e, 0x80, 0xf9, 0x16, 0xe5, 0x30, 0x9b, 0xbd, + 0xb2, 0x20, 0x64, 0x89, 0xe4, 0xed, 0xa4, 0x34, 0xf4, 0x57, 0x1a, 0x4c, 0x3f, 0x74, 0x4e, 0x1a, + 0x93, 0x3f, 0x82, 0xb7, 0xd2, 0x27, 0xf1, 0xa9, 0x69, 0xed, 0x31, 0xa4, 0xc9, 0x94, 0x56, 0x1f, + 0x33, 0x2e, 0x76, 0x0f, 0xf5, 0x88, 0xae, 0x73, 0xee, 0x56, 0x48, 0x2a, 0x40, 0x12, 0xbd, 0x16, + 0xee, 0xf9, 0x14, 0xb9, 0x8a, 0x18, 0xe4, 0xeb, 0x9a, 0x31, 0x17, 0xf3, 0xaa, 0x82, 0xb5, 0x75, + 0xae, 0xeb, 0xa5, 0xba, 0x00, 0xf3, 0xc9, 0x5e, 0x07, 0xfe, 0x57, 0x48, 0xcd, 0x96, 0x1f, 0x79, + 0xed, 0x1e, 0x7a, 0x14, 0x04, 0x31, 0x5d, 0xff, 0x06, 0x0a, 0xf2, 0xe0, 0xaa, 0x7b, 0xdc, 0x87, + 0x9c, 0x0a, 0xf6, 0x69, 0x46, 0x88, 0x78, 0x66, 0x49, 0x74, 0x87, 0x15, 0x42, 0x66, 0x48, 0x21, + 0xe8, 0x0f, 0xe1, 0xd2, 0x26, 0x32, 0x31, 0xe0, 0xab, 0xb1, 0xfe, 0x1c, 0x97, 0xb6, 0xfe, 0xbb, + 0x06, 0x97, 0x86, 0x3e, 0x16, 0xf8, 0x0d, 0x7b, 0x88, 0x34, 0x74, 0x7c, 0x4f, 0x18, 0xcc, 0x1a, + 0xf1, 0x92, 0xac, 0xc3, 0x84, 0xcc, 0xf5, 0x33, 0xbc, 0x7b, 0xa4, 0x26, 0xb9, 0x0b, 0x8b, 0xb8, + 0xb7, 0x87, 0x36, 0x73, 0x0e, 0xd1, 0xdc, 0xa3, 0xbe, 0xcb, 0x3b, 0x8f, 0xeb, 0xb0, 0x54, 0xfd, + 0x2d, 0x24, 0x02, 0xf7, 0xa9, 0xef, 0xde, 0x13, 0x6c, 0x5e, 0x71, 0x3a, 0xcc, 0x48, 0xf7, 0xe3, + 0xe2, 0x18, 0x97, 0x9d, 0x51, 0x11, 0x79, 0x6d, 0xe8, 0xdf, 0x6a, 0xb0, 0xd0, 0x0f, 0x92, 0x0a, + 0xd7, 0x70, 0x94, 0xde, 0xfc, 0x03, 0xeb, 0xe6, 0x01, 0xcc, 0xf6, 0x09, 0x91, 0x2b, 0x50, 0x7a, + 0xbc, 0x5b, 0xdb, 0xad, 0x99, 0xb5, 0xed, 0xf5, 0xea, 0xc3, 0xda, 0x56, 0x6d, 0xfb, 0x89, 0xb9, + 0xbb, 0xfd, 0x60, 0xfb, 0xd1, 0x17, 0xdb, 0x73, 0x63, 0x43, 0xb9, 0xf2, 0x73, 0x63, 0x4e, 0x23, + 0x4b, 0xb0, 0x38, 0xc0, 0xdd, 0x68, 0x34, 0x25, 0x3b, 0xb3, 0xf6, 0xd7, 0x14, 0xe4, 0x9b, 0xb1, + 0x77, 0xe4, 0x29, 0x8c, 0xf3, 0xc7, 0x34, 0xb9, 0x3e, 0xb2, 0x2b, 0x77, 0x1f, 0xe6, 0xe5, 0x95, + 0x93, 0x05, 0x25, 0x7e, 0xfa, 0x18, 0xb1, 0x60, 0x52, 0x3e, 0x57, 0xc9, 0x8d, 0x51, 0x5a, 0x3d, + 0xcf, 0xe8, 0xf2, 0xcd, 0xd7, 0x11, 0x4d, 0xb6, 0xf8, 0x41, 0x13, 0x49, 0x3e, 0xf8, 0xe4, 0x20, + 0x77, 0x46, 0xd9, 0x19, 0xf5, 0xa0, 0x2d, 0xdf, 0x3d, 0x83, 0x66, 0xe2, 0xd0, 0x8f, 0x32, 0xa1, + 0x86, 0xbc, 0x25, 0xc8, 0x69, 0xed, 0x76, 0xc7, 0xec, 0xf2, 0x27, 0x67, 0x51, 0x4d, 0x7c, 0xfa, + 0x5e, 0x83, 0xf9, 0x61, 0x33, 0x2b, 0xf9, 0xf8, 0x04, 0xb3, 0xc7, 0x3d, 0x52, 0xca, 0x77, 0x4e, + 0xaf, 0xd8, 0x1f, 0xb2, 0xc1, 0x31, 0x8b, 0x9c, 0xd2, 0x6a, 0x0a, 0x9f, 0xbb, 0x67, 0xd0, 0x4c, + 0x1c, 0x7a, 0x0a, 0xe3, 0xbc, 0x4f, 0x8f, 0xae, 0x81, 0xd4, 0x15, 0x36, 0xba, 0x06, 0xd2, 0x2d, + 0x5f, 0x1f, 0x23, 0x47, 0x50, 0xec, 0xed, 0x2f, 0xe4, 0xf6, 0x09, 0xbe, 0x0e, 0x36, 0xec, 0xf2, + 0xda, 0x69, 0x54, 0xe2, 0xad, 0xab, 0x08, 0x57, 0x6d, 0xdf, 0x1d, 0xa1, 0x5a, 0x2d, 0x26, 0x8d, + 0x60, 0x87, 0xfa, 0xcc, 0xdf, 0xd1, 0xbe, 0xbc, 0xdd, 0x71, 0xd8, 0x7e, 0xd4, 0xaa, 0xd8, 0xbe, + 0xbb, 0xca, 0x15, 0x57, 0x53, 0x8a, 0xab, 0x56, 0xe0, 0xac, 0x26, 0xca, 0xab, 0xe2, 0x9f, 0x5e, + 0xd0, 0x6a, 0x4d, 0x8a, 0x8f, 0x0f, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xc2, 0xb4, 0xde, 0xd9, + 0xef, 0x13, 0x00, 0x00, }, } diff --git a/doc/rfc/stovepipe/list-api.md b/doc/rfc/stovepipe/list-api.md index 6cc298175..70eb55721 100644 --- a/doc/rfc/stovepipe/list-api.md +++ b/doc/rfc/stovepipe/list-api.md @@ -42,12 +42,18 @@ This is the wire representation of the existing domain `RequestSummary`, not ano | `state_updated_at_ms` | int64 | Timestamp of the represented state entry. | | `accepted_at_ms` | int64 | Timestamp of the original retained accepted entry; `0` means unknown, otherwise positive and immutable. | | `outcome_reason` | string | Reason for the represented state; empty when unavailable or inapplicable. | +| `project_count` | int32 | Total recorded project results; zero means none recorded. | +| `broken_project_count` | int32 | Recorded project results with degree above zero. | The domain already defines a typed [RequestState](../../../stovepipe/entity/request.go). The wire field remains a string to match existing status/history APIs. States and reasons use the existing [public vocabulary](request-log.md#outcome-reasons); clients tolerate future values. Duplicate Ingest calls resolving to the same request produce one row. ## Data and Storage -All listed fields are materialized in [RequestSummary](../../../stovepipe/entity/request_summary.go), including acceptance time and outcome reason from retained logs. Acceptance time is acceptance-log time, not first RPC receipt time. No new producer signal or per-queue counter is needed. +All listed fields are materialized in [RequestSummary](../../../stovepipe/entity/request_summary.go), including acceptance time and outcome reason from retained logs. Acceptance time is acceptance-log time, not first RPC receipt time. The `project_facts_recorded` event supplies total and broken counts after all project facts and their indexes are durable. Counts follow persisted facts, including adopted immutable facts, rather than retry-time resolver answers. No per-queue counter is needed. + +Counts default to zero before project results are recorded, for empty results, and for older summaries without counts. Clients can show the ratio when `project_count` is positive. Lifecycle updates preserve counts, and completion projection preserves lifecycle timestamps and outcome reasons. + +Existing databases must add the defaulted `project_count` and `broken_project_count` summary columns before deploying readers or writers that use them. Defaults leave older counts at zero; historical count backfill is not automatic. Listing uses an immutable mapping keyed by `(queue, accepted_at_ms, request_id)` for known positive acceptance times, then point-reads the corresponding summaries. This adds one small record per request and bounded extra reads, not another mutable status projection. Existing summary keys and public request IDs remain unchanged; no numeric-key migration is needed. diff --git a/doc/rfc/stovepipe/request-history-api.md b/doc/rfc/stovepipe/request-history-api.md index e2a6a9d09..9d9632eaf 100644 --- a/doc/rfc/stovepipe/request-history-api.md +++ b/doc/rfc/stovepipe/request-history-api.md @@ -14,7 +14,7 @@ Request-ID lookup validates the queue and ID, then reads that queue's `RequestLo At least one retained log record defines the existence of a history. A summary alone does not establish a retained history, and the operational `Request` entity is not required for either read. A returned summary must match the selected queue and request ID; disagreement is an infrastructure consistency error. -The summary is the existing current lifecycle projection maintained by the request-log materializer. It includes request identity, queue, commit URI, baseline URI, current state, state timestamp, acceptance timestamp, and outcome reason. The mapper reuses the public summary representation exposed by List, omitting internal storage versions. +The summary is the existing current lifecycle projection maintained by the request-log materializer. It includes request identity, queue, commit URI, baseline URI, current state, state timestamp, acceptance timestamp, outcome reason, and recorded project outcome counts. The mapper reuses the public summary representation exposed by List, omitting internal storage versions. History is read before its summary. The two reads are eventually consistent, not a transactional snapshot: the current summary can represent newer state than the retained events returned by the earlier read, or lag a newly appended state event. The API does not replay the returned events to derive a summary or fabricate missing occurrences from current state. diff --git a/doc/rfc/stovepipe/request-log.md b/doc/rfc/stovepipe/request-log.md index 77a81ea96..1dc37cd87 100644 --- a/doc/rfc/stovepipe/request-log.md +++ b/doc/rfc/stovepipe/request-log.md @@ -131,7 +131,7 @@ Immutable Request context such as URI, build strategy, base URI, and terminal bu | `build_analysis_abandoned` | Build analysis could not complete; issue observations may be incomplete. This is not a validation verdict. | Build ID metadata and event retention time | | `issue_tracking_abandoned` | Issue tracking stopped before completing a pass; existing issue changes remain, but reconciliation may be incomplete. This is not a validation verdict. | Build ID metadata and event retention time | -Build running and unchanged polls are not retained. Trigger and terminal result explain the request outcome without turning polling into an unbounded log. Individual project facts remain outside the vocabulary; one batch-completion event summarizes their durable recording. +Build running and unchanged polls are not retained. Trigger and terminal result explain the request outcome without turning polling into an unbounded log. Individual project facts remain outside the vocabulary; one batch-completion event summarizes their durable recording. Its `project_fact_count` and `broken_project_fact_count` metadata contain the total and the number with degree above zero, counted from persisted facts after their indexes are durable. Project completion projection requires both counts and assigns them together in `RequestSummary` from the retained event. ### Evolution diff --git a/doc/rfc/stovepipe/steps/record.md b/doc/rfc/stovepipe/steps/record.md index e420f1ae0..d0ab56539 100644 --- a/doc/rfc/stovepipe/steps/record.md +++ b/doc/rfc/stovepipe/steps/record.md @@ -123,7 +123,7 @@ Whole-repository degree depends on the outcome `buildsignal` stamped on the Requ | `cancelled` | no fact | -`DegreePartial` is hard coded to `0.5` for now; it does not measure the fraction of projects failing. Each project fact preserves the degree returned by the resolver. +`DegreePartial` is hard coded to `0.5` for now; it does not measure the fraction of projects failing. Each project fact preserves the degree returned by the resolver, unless its immutable identity already exists. After persisting every project fact and its degree index, `record` retains total and broken project counts in `project_facts_recorded` for the request summary. Broken means a persisted degree above zero; retries count adopted facts rather than contradictory resolver results. Empty results record zero counts. ## Last-green advancement (Queue bookmark) diff --git a/service/stovepipe/server/mapper/list.go b/service/stovepipe/server/mapper/list.go index 693b9260f..ae8694737 100644 --- a/service/stovepipe/server/mapper/list.go +++ b/service/stovepipe/server/mapper/list.go @@ -44,13 +44,15 @@ func ListResultToProto(result entity.ListResult) *pb.ListResponse { // RequestSummaryToProto exposes public summary fields, omitting internal storage versions. func RequestSummaryToProto(summary entity.RequestSummary) *pb.RequestSummary { return &pb.RequestSummary{ - RequestId: summary.RequestID, - Queue: summary.Queue, - ChangeUri: summary.URI, - BaseUri: summary.BaseURI, - RequestState: string(summary.State), - StateUpdatedAtMs: summary.StateTimestampMs, - AcceptedAtMs: summary.AcceptedAtMs, - OutcomeReason: string(summary.OutcomeReason), + RequestId: summary.RequestID, + Queue: summary.Queue, + ChangeUri: summary.URI, + BaseUri: summary.BaseURI, + RequestState: string(summary.State), + StateUpdatedAtMs: summary.StateTimestampMs, + AcceptedAtMs: summary.AcceptedAtMs, + OutcomeReason: string(summary.OutcomeReason), + ProjectCount: summary.ProjectCount, + BrokenProjectCount: summary.BrokenProjectCount, } } diff --git a/service/stovepipe/server/mapper/list_test.go b/service/stovepipe/server/mapper/list_test.go index 19f4a3ec9..69cc20ea4 100644 --- a/service/stovepipe/server/mapper/list_test.go +++ b/service/stovepipe/server/mapper/list_test.go @@ -123,3 +123,22 @@ func TestListResultToProto(t *testing.T) { }) } } + +func TestListResultProjectCounts(t *testing.T) { + for _, tt := range []struct { + name string + count, broken int32 + }{ + {name: "no recorded project facts"}, + {name: "all green", count: 3}, + {name: "mixed outcomes", count: 3, broken: 1}, + } { + t.Run(tt.name, func(t *testing.T) { + response := ListResultToProto(entity.ListResult{Requests: []entity.RequestSummary{{ + RequestID: "7", ProjectCount: tt.count, BrokenProjectCount: tt.broken, + }}}) + require.Len(t, response.Requests, 1) + assert.Equal(t, &pb.RequestSummary{RequestId: "7", ProjectCount: tt.count, BrokenProjectCount: tt.broken}, response.Requests[0]) + }) + } +} diff --git a/service/stovepipe/server/mapper/request_history_test.go b/service/stovepipe/server/mapper/request_history_test.go index 0d7a83f6c..d278d05b1 100644 --- a/service/stovepipe/server/mapper/request_history_test.go +++ b/service/stovepipe/server/mapper/request_history_test.go @@ -180,6 +180,7 @@ func TestRequestHistorySummaryMapping(t *testing.T) { RequestSummary: entity.RequestSummary{ RequestID: "7", Queue: "queue", URI: "uri", BaseURI: "base", State: "future-state", StateTimestampMs: 200, AcceptedAtMs: 100, OutcomeReason: "future-reason", + ProjectCount: 3, BrokenProjectCount: 1, }, } byID := RequestHistoryByIDToProto(history) @@ -188,6 +189,7 @@ func TestRequestHistorySummaryMapping(t *testing.T) { want := &pb.RequestSummary{ RequestId: "7", Queue: "queue", ChangeUri: "uri", BaseUri: "base", RequestState: "future-state", StateUpdatedAtMs: 200, AcceptedAtMs: 100, OutcomeReason: "future-reason", + ProjectCount: 3, BrokenProjectCount: 1, } assert.Equal(t, want, byID.RequestSummary) assert.Equal(t, want, byURI.RequestSummary) diff --git a/stovepipe/controller/record/record.go b/stovepipe/controller/record/record.go index 33d9b24d5..7f80d7b39 100644 --- a/stovepipe/controller/record/record.go +++ b/stovepipe/controller/record/record.go @@ -209,6 +209,7 @@ func (c *Controller) persistProjectFacts(ctx context.Context, store storage.Stor op := metrics.Begin(c.metricsScope, "persist_project_facts", metrics.LongLatencyBuckets, metrics.TagsFromContext(ctx)...) defer func() { op.Complete(retErr) }() + brokenCount := 0 for _, result := range results { fact, _, err := recordValidationFact(ctx, c.logger, c.metricsScope, store.GetValidationFactStore(), entity.ValidationFact{ URI: request.URI, @@ -220,6 +221,9 @@ func (c *Controller) persistProjectFacts(ctx context.Context, store storage.Stor if err != nil { return err } + if !fact.IsGreen() { + brokenCount++ + } entry, err := entity.NewProjectFactByDegree(fact) if err != nil { @@ -230,7 +234,7 @@ func (c *Controller) persistProjectFacts(ctx context.Context, store storage.Stor return fmt.Errorf("failed to index project fact for request %q and project %q: %w", request.ID, fact.Project, err) } } - return c.persistProjectFactsRecordedLog(ctx, store, request, len(results)) + return c.persistProjectFactsRecordedLog(ctx, store, request, len(results), brokenCount) } func validateProjectStatusResults(request entity.Request, results []projectstatus.Result) error { @@ -254,13 +258,16 @@ func (c *Controller) persistProjectFactsRecordedLog( ctx context.Context, store storage.Storage, request entity.Request, - count int, + count, brokenCount int, ) error { log := requestlog.NewRequestEventLog( request, entity.RequestEventProjectFactsRecorded, "all", - map[string]string{requestlog.MetadataKeyProjectFactCount: strconv.Itoa(count)}, + map[string]string{ + requestlog.MetadataKeyProjectFactCount: strconv.Itoa(count), + requestlog.MetadataKeyBrokenProjectFactCount: strconv.Itoa(brokenCount), + }, ) if err := c.materializer.PersistLog(ctx, store, log); err != nil { return fmt.Errorf("failed to record project facts for request %s: %w", request.ID, err) diff --git a/stovepipe/controller/record/record_test.go b/stovepipe/controller/record/record_test.go index 074946d7b..57aca5d62 100644 --- a/stovepipe/controller/record/record_test.go +++ b/stovepipe/controller/record/record_test.go @@ -338,7 +338,7 @@ func TestProcess_RecordsNamedProjectStatusResults(t *testing.T) { gomock.Any(), requestWithState(entity.RequestStateFailed), "bk-1", ).Return([]projectstatus.Result{ {Project: "project-a", Degree: entity.DegreeBroken}, - {Project: "project-b", Degree: entity.DegreeBroken}, + {Project: "project-b", Degree: entity.DegreeGreen}, }, nil) m.reqStore.EXPECT().Get(gomock.Any(), testID).Return(requestWithState(entity.RequestStateFailed), nil) @@ -379,6 +379,8 @@ func TestProcess_RecordsNamedProjectStatusResults(t *testing.T) { assert.Equal(t, testID, fact.RequestID) if fact.Project == "" { assert.Equal(t, entity.DegreePartial, fact.Degree) + } else if fact.Project == "project-b" { + assert.Equal(t, entity.DegreeGreen, fact.Degree) } else { assert.Equal(t, entity.DegreeBroken, fact.Degree) } @@ -390,6 +392,7 @@ func TestProcess_RecordsNamedProjectStatusResults(t *testing.T) { assert.Equal(t, "2", logs[1].Metadata[requestlog.MetadataKeyProjectFactCount]) assertRecordOperationMetrics(t, m.metricsScope, "resolve_project_results", "success") assertRecordOperationMetrics(t, m.metricsScope, "persist_project_facts", "success") + assert.Equal(t, "1", logs[1].Metadata[requestlog.MetadataKeyBrokenProjectFactCount]) } func TestRecordProjectFacts_PassesEmptyTerminalBuild(t *testing.T) { @@ -608,6 +611,7 @@ func TestProcessRecordsEmptyProjectCompletion(t *testing.T) { func(_ context.Context, _ storage.Storage, log entity.RequestLog) error { assert.Equal(t, "event/project_facts_recorded/all", log.ID) assert.Equal(t, "0", log.Metadata[requestlog.MetadataKeyProjectFactCount]) + assert.Equal(t, "0", log.Metadata[requestlog.MetadataKeyBrokenProjectFactCount]) return tt.writeErr }), ) @@ -1498,3 +1502,41 @@ func TestProcessRetriesProjectPersistenceWithImmutablePartialFact(t *testing.T) require.NoError(t, c.Process(queueContext(), delivery(t, ctrl, recordPayload(t, testID)))) assert.Len(t, m.hooks.events, 1) } + +func TestPersistProjectFactsCountsPersistedOutcomes(t *testing.T) { + for _, tt := range []struct { + name string + storedDegree, resolvedDegree float64 + broken string + }{ + {name: "new green", storedDegree: entity.DegreeGreen, resolvedDegree: entity.DegreeGreen, broken: "0"}, + {name: "new partially broken", storedDegree: 0.25, resolvedDegree: 0.25, broken: "1"}, + {name: "new fully broken", storedDegree: entity.DegreeBroken, resolvedDegree: entity.DegreeBroken, broken: "1"}, + {name: "adopts existing green", storedDegree: entity.DegreeGreen, resolvedDegree: entity.DegreeBroken, broken: "0"}, + {name: "adopts existing broken", storedDegree: entity.DegreeBroken, resolvedDegree: entity.DegreeGreen, broken: "1"}, + } { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + c, m := newController(t, ctrl) + request := requestWithState(entity.RequestStateFailed) + stored := entity.ValidationFact{URI: request.URI, Project: "p", Degree: tt.storedDegree, RequestID: request.ID, CreatedAt: 10} + if tt.storedDegree != tt.resolvedDegree { + m.factStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(storage.ErrAlreadyExists) + m.factStore.EXPECT().Get(gomock.Any(), request.URI, "p").Return(stored, nil) + } else { + m.factStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) + } + m.indexStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) + materializer := requestlogmock.NewMockMaterializer(ctrl) + c.materializer = materializer + materializer.EXPECT().PersistLog(gomock.Any(), m.store, gomock.Any()).DoAndReturn(func(_ context.Context, _ storage.Storage, log entity.RequestLog) error { + assert.Equal(t, map[string]string{ + requestlog.MetadataKeyProjectFactCount: "1", + requestlog.MetadataKeyBrokenProjectFactCount: tt.broken, + }, log.Metadata) + return nil + }) + require.NoError(t, c.persistProjectFacts(queueContext(), m.store, request, []projectstatus.Result{{Project: "p", Degree: tt.resolvedDegree}})) + }) + } +} diff --git a/stovepipe/core/requestlog/materializer.go b/stovepipe/core/requestlog/materializer.go index 2eb424ae7..a90f79348 100644 --- a/stovepipe/core/requestlog/materializer.go +++ b/stovepipe/core/requestlog/materializer.go @@ -42,6 +42,8 @@ const ( MetadataKeyFactDegree = "fact_degree" // MetadataKeyProjectFactCount records how many project facts a completed batch established. MetadataKeyProjectFactCount = "project_fact_count" + // MetadataKeyBrokenProjectFactCount records how many persisted project facts have a nonzero degree. + MetadataKeyBrokenProjectFactCount = "broken_project_fact_count" // MetadataKeyBaseURI records the selected build baseline; an empty value means a full build. MetadataKeyBaseURI = "base_uri" ) diff --git a/stovepipe/core/requestlog/request_summary.go b/stovepipe/core/requestlog/request_summary.go index 43d8551e1..230c6edc4 100644 --- a/stovepipe/core/requestlog/request_summary.go +++ b/stovepipe/core/requestlog/request_summary.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/uber/submitqueue/stovepipe/entity" "github.com/uber/submitqueue/stovepipe/extension/storage" @@ -121,6 +122,7 @@ func projectRequestSummaryFacts(summary entity.RequestSummary, log entity.Reques return entity.RequestSummary{}, fmt.Errorf("request summary completion time conflicts with retained log request_id=%q", log.RequestID) } summary.ProjectResultsCompletedAtMs = log.TimestampMs + return projectRequestSummaryProjectCounts(summary, log) } if log.State == entity.RequestStateAccepted { if summary.AcceptedAtMs != 0 && summary.AcceptedAtMs != log.TimestampMs { @@ -131,6 +133,17 @@ func projectRequestSummaryFacts(summary entity.RequestSummary, log entity.Reques return summary, nil } +func projectRequestSummaryProjectCounts(summary entity.RequestSummary, log entity.RequestLog) (entity.RequestSummary, error) { + count, countErr := strconv.ParseInt(log.Metadata[MetadataKeyProjectFactCount], 10, 32) + broken, brokenErr := strconv.ParseInt(log.Metadata[MetadataKeyBrokenProjectFactCount], 10, 32) + if countErr != nil || brokenErr != nil || count < 0 || broken < 0 || broken > count { + return entity.RequestSummary{}, fmt.Errorf("invalid project outcome counts in retained log request_id=%q", log.RequestID) + } + summary.ProjectCount = int32(count) + summary.BrokenProjectCount = int32(broken) + return summary, nil +} + func projectRequestSummaryState(summary entity.RequestSummary, log entity.RequestLog) (entity.RequestSummary, error) { if log.State == entity.RequestStateUnknown { return summary, nil diff --git a/stovepipe/core/requestlog/request_summary_test.go b/stovepipe/core/requestlog/request_summary_test.go index eecbafa77..e98aa7b07 100644 --- a/stovepipe/core/requestlog/request_summary_test.go +++ b/stovepipe/core/requestlog/request_summary_test.go @@ -211,6 +211,8 @@ func TestMaterializerRetriesRequestSummaryVersionMismatch(t *testing.T) { winner := current winner.Version = 5 winner.ProjectResultsCompletedAtMs = testNowMs - 500 + winner.ProjectCount = 3 + winner.BrokenProjectCount = 1 updated := winner updated.State = log.State updated.RequestVersion = log.RequestVersion @@ -218,6 +220,8 @@ func TestMaterializerRetriesRequestSummaryVersionMismatch(t *testing.T) { firstAttempt := updated firstAttempt.Version = current.Version firstAttempt.ProjectResultsCompletedAtMs = 0 + firstAttempt.ProjectCount = 0 + firstAttempt.BrokenProjectCount = 0 gomock.InOrder( requestLogs.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil), summaries.EXPECT().Get(gomock.Any(), testRequestID).Return(current, nil), @@ -509,19 +513,20 @@ func TestMaterializerRetriesAcceptanceRepairAfterPartialWrite(t *testing.T) { func TestMaterializerProjectsProjectCompletion(t *testing.T) { for _, tt := range []struct { - name string - completedAt int64 - count string - wantErr bool + name string + completedAt int64 + count string + projectCount int32 + wantErr bool }{ - {name: "completed projects", count: "3"}, + {name: "completed projects", count: "3", projectCount: 3}, {name: "completed empty set", count: "0"}, - {name: "duplicate completion", completedAt: testNowMs, count: "3"}, + {name: "duplicate completion", completedAt: testNowMs, count: "3", projectCount: 3}, {name: "conflicting timestamp", completedAt: testNowMs - 1, count: "3", wantErr: true}, } { t.Run(tt.name, func(t *testing.T) { materializer, stores, logs, summaries, _ := newTestMaterializer(t) - log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{MetadataKeyProjectFactCount: tt.count}) + log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{MetadataKeyProjectFactCount: tt.count, MetadataKeyBrokenProjectFactCount: "0"}) log.TimestampMs = testNowMs current := seededRequestSummary() current.State = entity.RequestStateFailed @@ -533,6 +538,7 @@ func TestMaterializerProjectsProjectCompletion(t *testing.T) { current.ProjectResultsCompletedAtMs = tt.completedAt want := current want.ProjectResultsCompletedAtMs = log.TimestampMs + want.ProjectCount = tt.projectCount logs.EXPECT().Create(gomock.Any(), log).Return(storage.ErrAlreadyExists) logs.EXPECT().Get(gomock.Any(), testRequestID, log.ID).Return(log, nil) summaries.EXPECT().Get(gomock.Any(), testRequestID).Return(current, nil) @@ -551,7 +557,7 @@ func TestMaterializerProjectsProjectCompletion(t *testing.T) { func TestMaterializerRepairsCompletionAfterProjectionFailure(t *testing.T) { materializer, stores, logs, summaries, _ := newTestMaterializer(t) - log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", nil) + log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "1"}) retained := log retained.TimestampMs = testNowMs current := seededRequestSummary() @@ -560,6 +566,8 @@ func TestMaterializerRepairsCompletionAfterProjectionFailure(t *testing.T) { current.StateTimestampMs = testNowMs - 1000 updated := current updated.ProjectResultsCompletedAtMs = retained.TimestampMs + updated.ProjectCount = 3 + updated.BrokenProjectCount = 1 writeErr := errors.New("summary unavailable") gomock.InOrder( logs.EXPECT().Create(gomock.Any(), retained).Return(nil), @@ -582,13 +590,15 @@ func TestMaterializerRepairsCompletionAfterProjectionFailure(t *testing.T) { func TestMaterializerRetriesCompletionAfterStateProjectionWins(t *testing.T) { materializer, stores, logs, summaries, _ := newTestMaterializer(t) - log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", nil) + log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "1"}) log.TimestampMs = testNowMs current := seededRequestSummary() current.State = entity.RequestStateProcessing current.RequestVersion = 2 updated := current updated.ProjectResultsCompletedAtMs = log.TimestampMs + updated.ProjectCount = 3 + updated.BrokenProjectCount = 1 winner := current winner.State = entity.RequestStateFailed winner.RequestVersion = 3 @@ -597,6 +607,8 @@ func TestMaterializerRetriesCompletionAfterStateProjectionWins(t *testing.T) { winner.Version++ updatedWinner := winner updatedWinner.ProjectResultsCompletedAtMs = log.TimestampMs + updatedWinner.ProjectCount = 3 + updatedWinner.BrokenProjectCount = 1 gomock.InOrder( logs.EXPECT().Create(gomock.Any(), log).Return(nil), summaries.EXPECT().Get(gomock.Any(), testRequestID).Return(current, nil), @@ -614,3 +626,62 @@ func TestMaterializerCompletionRequiresExistingSummary(t *testing.T) { summaries.EXPECT().Get(gomock.Any(), testRequestID).Return(entity.RequestSummary{}, storage.ErrNotFound) require.ErrorIs(t, materializer.PersistLog(context.Background(), stores, log), storage.ErrNotFound) } + +func TestProjectRequestSummaryProjectCounts(t *testing.T) { + for _, tt := range []struct { + name string + metadata map[string]string + count, broken int32 + wantErr bool + }{ + {name: "mixed outcomes", metadata: map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "2"}, count: 3, broken: 2}, + {name: "all green", metadata: map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "0"}, count: 3}, + {name: "empty results", metadata: map[string]string{MetadataKeyProjectFactCount: "0", MetadataKeyBrokenProjectFactCount: "0"}}, + {name: "missing broken", metadata: map[string]string{MetadataKeyProjectFactCount: "3"}, wantErr: true}, + {name: "missing counts", wantErr: true}, + {name: "missing total", metadata: map[string]string{MetadataKeyBrokenProjectFactCount: "1"}, wantErr: true}, + {name: "invalid total", metadata: map[string]string{MetadataKeyProjectFactCount: "oops", MetadataKeyBrokenProjectFactCount: "1"}, wantErr: true}, + {name: "invalid broken", metadata: map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "oops"}, wantErr: true}, + {name: "negative total", metadata: map[string]string{MetadataKeyProjectFactCount: "-1", MetadataKeyBrokenProjectFactCount: "0"}, wantErr: true}, + {name: "negative broken", metadata: map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "-1"}, wantErr: true}, + {name: "broken exceeds total", metadata: map[string]string{MetadataKeyProjectFactCount: "3", MetadataKeyBrokenProjectFactCount: "4"}, wantErr: true}, + {name: "overflow", metadata: map[string]string{MetadataKeyProjectFactCount: "2147483648", MetadataKeyBrokenProjectFactCount: "1"}, wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + current := seededRequestSummary() + log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", tt.metadata) + log.TimestampMs = testNowMs + got, err := projectRequestSummary(current, log) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + want := current + want.ProjectResultsCompletedAtMs = testNowMs + want.ProjectCount = tt.count + want.BrokenProjectCount = tt.broken + require.Equal(t, want, got) + replayed, err := projectRequestSummary(got, log) + require.NoError(t, err) + require.Equal(t, got, replayed) + }) + } +} + +func TestProjectRequestSummaryUsesRetainedCounts(t *testing.T) { + current := seededRequestSummary() + current.ProjectResultsCompletedAtMs = testNowMs + current.ProjectCount = 3 + current.BrokenProjectCount = 1 + log := NewRequestEventLog(entity.Request{ID: testRequestID, Queue: testQueue}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{ + MetadataKeyProjectFactCount: "4", MetadataKeyBrokenProjectFactCount: "2", + }) + log.TimestampMs = testNowMs + got, err := projectRequestSummary(current, log) + require.NoError(t, err) + want := current + want.ProjectCount = 4 + want.BrokenProjectCount = 2 + require.Equal(t, want, got) +} diff --git a/stovepipe/entity/request_summary.go b/stovepipe/entity/request_summary.go index b40ac23f1..451521e6a 100644 --- a/stovepipe/entity/request_summary.go +++ b/stovepipe/entity/request_summary.go @@ -34,6 +34,10 @@ type RequestSummary struct { AcceptedAtMs int64 // ProjectResultsCompletedAtMs is the completion timestamp in Unix milliseconds; zero means unknown and a positive value is immutable. ProjectResultsCompletedAtMs int64 + // ProjectCount is the number of recorded project results; zero means none recorded. + ProjectCount int32 + // BrokenProjectCount counts recorded project results with degree above DegreeGreen. + BrokenProjectCount int32 // OutcomeReason is the reason for the represented state; empty means unavailable or inapplicable. OutcomeReason RequestOutcomeReason // Version is the optimistic-lock version of this materialized view. diff --git a/stovepipe/extension/storage/mysql/request_summary_store.go b/stovepipe/extension/storage/mysql/request_summary_store.go index aa6932434..9c14a24b4 100644 --- a/stovepipe/extension/storage/mysql/request_summary_store.go +++ b/stovepipe/extension/storage/mysql/request_summary_store.go @@ -48,8 +48,8 @@ func (s *requestSummaryStore) Create(ctx context.Context, summary entity.Request _, err := s.db.ExecContext(ctx, ` INSERT INTO request_summary ( - queue, request_id, uri, base_uri, state, request_version, state_timestamp_ms, accepted_at_ms, outcome_reason, project_results_completed_at_ms, version - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + queue, request_id, uri, base_uri, state, request_version, state_timestamp_ms, accepted_at_ms, outcome_reason, project_results_completed_at_ms, project_count, broken_project_count, version + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, summary.Queue, summary.RequestID, summary.URI, @@ -60,6 +60,8 @@ func (s *requestSummaryStore) Create(ctx context.Context, summary entity.Request summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, + summary.ProjectCount, + summary.BrokenProjectCount, summary.Version, ) if err != nil { @@ -76,7 +78,7 @@ func (s *requestSummaryStore) Get(ctx context.Context, requestID string) (ret en defer func() { op.Complete(retErr) }() err := s.db.QueryRowContext(ctx, ` - SELECT queue, request_id, uri, base_uri, state, request_version, state_timestamp_ms, accepted_at_ms, outcome_reason, project_results_completed_at_ms, version + SELECT queue, request_id, uri, base_uri, state, request_version, state_timestamp_ms, accepted_at_ms, outcome_reason, project_results_completed_at_ms, project_count, broken_project_count, version FROM request_summary WHERE queue = ? AND request_id = ?`, s.queue, requestID, @@ -91,6 +93,8 @@ func (s *requestSummaryStore) Get(ctx context.Context, requestID string) (ret en &ret.AcceptedAtMs, &ret.OutcomeReason, &ret.ProjectResultsCompletedAtMs, + &ret.ProjectCount, + &ret.BrokenProjectCount, &ret.Version, ) if errors.Is(err, sql.ErrNoRows) { @@ -112,7 +116,7 @@ func (s *requestSummaryStore) Update(ctx context.Context, summary entity.Request result, err := s.db.ExecContext(ctx, ` UPDATE request_summary - SET base_uri = ?, state = ?, request_version = ?, state_timestamp_ms = ?, accepted_at_ms = ?, outcome_reason = ?, project_results_completed_at_ms = ?, version = ? + SET base_uri = ?, state = ?, request_version = ?, state_timestamp_ms = ?, accepted_at_ms = ?, outcome_reason = ?, project_results_completed_at_ms = ?, project_count = ?, broken_project_count = ?, version = ? WHERE queue = ? AND request_id = ? AND version = ?`, summary.BaseURI, summary.State, @@ -121,6 +125,8 @@ func (s *requestSummaryStore) Update(ctx context.Context, summary entity.Request summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, + summary.ProjectCount, + summary.BrokenProjectCount, newVersion, summary.Queue, summary.RequestID, diff --git a/stovepipe/extension/storage/mysql/request_summary_store_test.go b/stovepipe/extension/storage/mysql/request_summary_store_test.go index 54eaf33dc..f7aa09922 100644 --- a/stovepipe/extension/storage/mysql/request_summary_store_test.go +++ b/stovepipe/extension/storage/mysql/request_summary_store_test.go @@ -43,6 +43,8 @@ func testRequestSummary() entity.RequestSummary { AcceptedAtMs: 1735689599000, OutcomeReason: entity.RequestOutcomeReasonProcessingFailed, ProjectResultsCompletedAtMs: 1735689601000, + ProjectCount: 7, + BrokenProjectCount: 2, Version: 1, } } @@ -68,7 +70,7 @@ func TestRequestSummaryStoreCreate(t *testing.T) { summary: summary, setup: func(mock sqlmock.Sqlmock) { mock.ExpectExec("INSERT INTO request_summary"). - WithArgs(summary.Queue, summary.RequestID, summary.URI, summary.BaseURI, summary.State, summary.RequestVersion, summary.StateTimestampMs, summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, summary.Version). + WithArgs(summary.Queue, summary.RequestID, summary.URI, summary.BaseURI, summary.State, summary.RequestVersion, summary.StateTimestampMs, summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, summary.ProjectCount, summary.BrokenProjectCount, summary.Version). WillReturnResult(sqlmock.NewResult(0, 1)) }, }, @@ -77,7 +79,7 @@ func TestRequestSummaryStoreCreate(t *testing.T) { summary: summary, setup: func(mock sqlmock.Sqlmock) { mock.ExpectExec("INSERT INTO request_summary"). - WithArgs(summary.Queue, summary.RequestID, summary.URI, summary.BaseURI, summary.State, summary.RequestVersion, summary.StateTimestampMs, summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, summary.Version). + WithArgs(summary.Queue, summary.RequestID, summary.URI, summary.BaseURI, summary.State, summary.RequestVersion, summary.StateTimestampMs, summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, summary.ProjectCount, summary.BrokenProjectCount, summary.Version). WillReturnError(&mysql.MySQLError{Number: mysqlErrDuplicateEntry}) }, wantErr: true, @@ -127,8 +129,8 @@ func TestRequestSummaryStoreGet(t *testing.T) { { name: "found", setup: func(mock sqlmock.Sqlmock) { - rows := sqlmock.NewRows([]string{"queue", "request_id", "uri", "base_uri", "state", "request_version", "state_timestamp_ms", "accepted_at_ms", "outcome_reason", "project_results_completed_at_ms", "version"}). - AddRow(want.Queue, want.RequestID, want.URI, want.BaseURI, want.State, want.RequestVersion, want.StateTimestampMs, want.AcceptedAtMs, want.OutcomeReason, want.ProjectResultsCompletedAtMs, want.Version) + rows := sqlmock.NewRows([]string{"queue", "request_id", "uri", "base_uri", "state", "request_version", "state_timestamp_ms", "accepted_at_ms", "outcome_reason", "project_results_completed_at_ms", "project_count", "broken_project_count", "version"}). + AddRow(want.Queue, want.RequestID, want.URI, want.BaseURI, want.State, want.RequestVersion, want.StateTimestampMs, want.AcceptedAtMs, want.OutcomeReason, want.ProjectResultsCompletedAtMs, want.ProjectCount, want.BrokenProjectCount, want.Version) mock.ExpectQuery("SELECT queue, request_id, uri, base_uri, state").WithArgs(testRequestSummaryQueue, want.RequestID).WillReturnRows(rows) }, }, @@ -181,7 +183,7 @@ func TestRequestSummaryStoreUpdate(t *testing.T) { db, mock, store := setupRequestSummaryStoreTest(t) defer db.Close() expectation := mock.ExpectExec("UPDATE request_summary"). - WithArgs(summary.BaseURI, summary.State, summary.RequestVersion, summary.StateTimestampMs, summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, newVersion, summary.Queue, summary.RequestID, oldVersion) + WithArgs(summary.BaseURI, summary.State, summary.RequestVersion, summary.StateTimestampMs, summary.AcceptedAtMs, summary.OutcomeReason, summary.ProjectResultsCompletedAtMs, summary.ProjectCount, summary.BrokenProjectCount, newVersion, summary.Queue, summary.RequestID, oldVersion) if tt.execErr != nil { expectation.WillReturnError(tt.execErr) } else { diff --git a/stovepipe/extension/storage/mysql/schema/request_summary.sql b/stovepipe/extension/storage/mysql/schema/request_summary.sql index 572e33264..373abd0a2 100644 --- a/stovepipe/extension/storage/mysql/schema/request_summary.sql +++ b/stovepipe/extension/storage/mysql/schema/request_summary.sql @@ -13,5 +13,7 @@ CREATE TABLE IF NOT EXISTS request_summary ( accepted_at_ms BIGINT NOT NULL DEFAULT 0, outcome_reason VARCHAR(64) NOT NULL DEFAULT '', project_results_completed_at_ms BIGINT NOT NULL DEFAULT 0, + project_count INT NOT NULL DEFAULT 0, + broken_project_count INT NOT NULL DEFAULT 0, PRIMARY KEY (queue, request_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/test/integration/stovepipe/extension/storage/mysql/storage_test.go b/test/integration/stovepipe/extension/storage/mysql/storage_test.go index 514bf5b06..fb8d05dcb 100644 --- a/test/integration/stovepipe/extension/storage/mysql/storage_test.go +++ b/test/integration/stovepipe/extension/storage/mysql/storage_test.go @@ -229,6 +229,9 @@ func (s *MySQLRequestSummaryStoreSuite) TestCreateGetAndUpdate() { updated.OutcomeReason = entity.RequestOutcomeReasonProcessingFailed updated.RequestVersion = 2 updated.StateTimestampMs = 2000 + updated.ProjectResultsCompletedAtMs = 2100 + updated.ProjectCount = 5 + updated.BrokenProjectCount = 2 require.NoError(s.T(), s.store.Update(s.ctx, updated, 1, 2)) updated.Version = 2 diff --git a/test/integration/stovepipe/suite_test.go b/test/integration/stovepipe/suite_test.go index ea868f694..a23a7e4b3 100644 --- a/test/integration/stovepipe/suite_test.go +++ b/test/integration/stovepipe/suite_test.go @@ -382,7 +382,7 @@ func (s *StovepipeIntegrationSuite) TestProjectStatusAPI() { assert.Equal(t, int64(4000), remaining.GetUpdatedAtMs()) assert.Empty(t, remaining.NextPageToken) assert.False(t, remaining.ProjectResultsComplete) - completion := requestlog.NewRequestEventLog(entity.Request{Queue: queue, ID: id}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{requestlog.MetadataKeyProjectFactCount: "3"}) + completion := requestlog.NewRequestEventLog(entity.Request{Queue: queue, ID: id}, entity.RequestEventProjectFactsRecorded, "all", map[string]string{requestlog.MetadataKeyProjectFactCount: "3", requestlog.MetadataKeyBrokenProjectFactCount: "2"}) completion.TimestampMs = 6000 backend, err := mysqlstorage.NewStorage(s.db, tally.NoopScope) require.NoError(t, err) diff --git a/web/api/src/gen/api/stovepipe/proto/stovepipe_pb.ts b/web/api/src/gen/api/stovepipe/proto/stovepipe_pb.ts index 5f59064af..9114a64b9 100644 --- a/web/api/src/gen/api/stovepipe/proto/stovepipe_pb.ts +++ b/web/api/src/gen/api/stovepipe/proto/stovepipe_pb.ts @@ -24,7 +24,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file api/stovepipe/proto/stovepipe.proto. */ export const file_api_stovepipe_proto_stovepipe: GenFile = /*@__PURE__*/ - fileDesc("CiNhcGkvc3RvdmVwaXBlL3Byb3RvL3N0b3ZlcGlwZS5wcm90bxIadWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUiHgoLUGluZ1JlcXVlc3QSDwoHbWVzc2FnZRgBIAEoCSJaCgxQaW5nUmVzcG9uc2USDwoHbWVzc2FnZRgBIAEoCRIUCgxzZXJ2aWNlX25hbWUYAiABKAkSEQoJdGltZXN0YW1wGAMgASgDEhAKCGhvc3RuYW1lGAQgASgJIl8KDUluZ2VzdFJlcXVlc3QSDQoFcXVldWUYASABKAkSPwoKZW5hYmxlbWVudBgCIAEoDjIrLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLlF1ZXVlRW5hYmxlbWVudCJjCg5Jbmdlc3RSZXNwb25zZRIKCgJpZBgBIAEoCRJFCgplbmFibGVtZW50GAIgASgLMjEudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUXVldWVFbmFibGVtZW50U3RhdHVzIkEKHEdldFJlcXVlc3RIaXN0b3J5QnlJRFJlcXVlc3QSDQoFcXVldWUYASABKAkSEgoKcmVxdWVzdF9pZBgCIAEoCSI7Ch1HZXRSZXF1ZXN0SGlzdG9yeUJ5VVJJUmVxdWVzdBINCgVxdWV1ZRgBIAEoCRILCgN1cmkYAiABKAkihgEKDEhpc3RvcnlFdmVudBIQCghldmVudF9pZBgBIAEoCRIUCgx0aW1lc3RhbXBfbXMYAiABKAMSFwoNcmVxdWVzdF9zdGF0ZRgDIAEoCUgAEg8KBWV2ZW50GAQgASgJSAASFgoOb3V0Y29tZV9yZWFzb24YBSABKAlCDAoKb2NjdXJyZW5jZSKjAQoOUmVxdWVzdEhpc3RvcnkSEgoKcmVxdWVzdF9pZBgBIAEoCRI4CgZldmVudHMYAiADKAsyKC51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5IaXN0b3J5RXZlbnQSQwoPcmVxdWVzdF9zdW1tYXJ5GAMgASgLMioudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUmVxdWVzdFN1bW1hcnkingEKHUdldFJlcXVlc3RIaXN0b3J5QnlJRFJlc3BvbnNlEjgKBmV2ZW50cxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkhpc3RvcnlFdmVudBJDCg9yZXF1ZXN0X3N1bW1hcnkYAiABKAsyKi51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5SZXF1ZXN0U3VtbWFyeSJfCh5HZXRSZXF1ZXN0SGlzdG9yeUJ5VVJJUmVzcG9uc2USPQoJaGlzdG9yaWVzGAEgAygLMioudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUmVxdWVzdEhpc3RvcnkieQobR2V0UHJvamVjdFN0YXR1c0J5SURSZXF1ZXN0Eg0KBXF1ZXVlGAEgASgJEhIKCnJlcXVlc3RfaWQYAiABKAkSEAoIcHJvamVjdHMYAyADKAkSEQoJcGFnZV9zaXplGAQgASgFEhIKCnBhZ2VfdG9rZW4YBSABKAkiegocR2V0UHJvamVjdFN0YXR1c0J5VVJJUmVxdWVzdBINCgVxdWV1ZRgBIAEoCRISCgpjaGFuZ2VfdXJpGAIgASgJEhAKCHByb2plY3RzGAMgAygJEhEKCXBhZ2Vfc2l6ZRgEIAEoBRISCgpwYWdlX3Rva2VuGAUgASgJIkkKEVByb2plY3RWYWxpZGF0aW9uEg8KB3Byb2plY3QYASABKAkSGQoPYnJlYWthZ2VfZGVncmVlGAIgASgBSABCCAoGcmVzdWx0IswCChxHZXRQcm9qZWN0U3RhdHVzQnlJRFJlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSDQoFcXVldWUYAiABKAkSEgoKY2hhbmdlX3VyaRgDIAEoCRIQCghiYXNlX3VyaRgEIAEoCRIVCg1yZXF1ZXN0X3N0YXRlGAUgASgJEhUKDXVwZGF0ZWRfYXRfbXMYCiABKAMSJAoacmVwb3NpdG9yeV9icmVha2FnZV9kZWdyZWUYBiABKAFIABIgChhwcm9qZWN0X3Jlc3VsdHNfY29tcGxldGUYByABKAgSPwoIcHJvamVjdHMYCCADKAsyLS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5Qcm9qZWN0VmFsaWRhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YCSABKAlCEwoRcmVwb3NpdG9yeV9yZXN1bHQizQIKHUdldFByb2plY3RTdGF0dXNCeVVSSVJlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSDQoFcXVldWUYAiABKAkSEgoKY2hhbmdlX3VyaRgDIAEoCRIQCghiYXNlX3VyaRgEIAEoCRIVCg1yZXF1ZXN0X3N0YXRlGAUgASgJEhUKDXVwZGF0ZWRfYXRfbXMYCiABKAMSJAoacmVwb3NpdG9yeV9icmVha2FnZV9kZWdyZWUYBiABKAFIABIgChhwcm9qZWN0X3Jlc3VsdHNfY29tcGxldGUYByABKAgSPwoIcHJvamVjdHMYCCADKAsyLS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5Qcm9qZWN0VmFsaWRhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YCSABKAlCEwoRcmVwb3NpdG9yeV9yZXN1bHQivQEKDlJlcXVlc3RTdW1tYXJ5EhIKCnJlcXVlc3RfaWQYASABKAkSDQoFcXVldWUYAiABKAkSEgoKY2hhbmdlX3VyaRgDIAEoCRIQCghiYXNlX3VyaRgEIAEoCRIVCg1yZXF1ZXN0X3N0YXRlGAUgASgJEhsKE3N0YXRlX3VwZGF0ZWRfYXRfbXMYBiABKAMSFgoOYWNjZXB0ZWRfYXRfbXMYByABKAMSFgoOb3V0Y29tZV9yZWFzb24YCCABKAkitAEKC0xpc3RSZXF1ZXN0Eg0KBXF1ZXVlGAEgASgJEiEKF2FjY2VwdGVkX2F0X29yX2FmdGVyX21zGAIgASgDSAASHAoSYWNjZXB0ZWRfYmVmb3JlX21zGAMgASgDSAESEQoJcGFnZV9zaXplGAQgASgFEhIKCnBhZ2VfdG9rZW4YBSABKAlCFgoUYWNjZXB0ZWRfbG93ZXJfYm91bmRCFgoUYWNjZXB0ZWRfdXBwZXJfYm91bmQiZQoMTGlzdFJlc3BvbnNlEjwKCHJlcXVlc3RzGAEgAygLMioudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUmVxdWVzdFN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjoKFUdldFF1ZXVlU3RhdHVzUmVxdWVzdBINCgVxdWV1ZRgBIAEoCRISCgpjaGFuZ2VfdXJpGAIgASgJIp4BChVRdWV1ZUVuYWJsZW1lbnRTdGF0dXMSDwoHdmVyc2lvbhgBIAEoAxI6CgVzdGF0ZRgCIAEoDjIrLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLlF1ZXVlRW5hYmxlbWVudBIhChllZmZlY3RpdmVfZnJvbV9jb21taXRfdXJpGAMgASgJEhUKDWNoYW5nZWRfYXRfbXMYBCABKAMibgoWR2V0UXVldWVTdGF0dXNSZXNwb25zZRINCgVxdWV1ZRgBIAEoCRJFCgplbmFibGVtZW50GAIgASgLMjEudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUXVldWVFbmFibGVtZW50U3RhdHVzKmwKD1F1ZXVlRW5hYmxlbWVudBIcChhRVUVVRV9FTkFCTEVNRU5UX1VOS05PV04QABIcChhRVUVVRV9FTkFCTEVNRU5UX0VOQUJMRUQQARIdChlRVUVVRV9FTkFCTEVNRU5UX0RJU0FCTEVEEAIy5wcKCVN0b3ZlcGlwZRJbCgRQaW5nEicudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUGluZ1JlcXVlc3QaKC51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5QaW5nUmVzcG9uc2UiABJhCgZJbmdlc3QSKS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5Jbmdlc3RSZXF1ZXN0GioudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuSW5nZXN0UmVzcG9uc2UiABKOAQoVR2V0UmVxdWVzdEhpc3RvcnlCeUlEEjgudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UmVxdWVzdEhpc3RvcnlCeUlEUmVxdWVzdBo5LnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFJlcXVlc3RIaXN0b3J5QnlJRFJlc3BvbnNlIgASkQEKFkdldFJlcXVlc3RIaXN0b3J5QnlVUkkSOS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5HZXRSZXF1ZXN0SGlzdG9yeUJ5VVJJUmVxdWVzdBo6LnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFJlcXVlc3RIaXN0b3J5QnlVUklSZXNwb25zZSIAEosBChRHZXRQcm9qZWN0U3RhdHVzQnlJRBI3LnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFByb2plY3RTdGF0dXNCeUlEUmVxdWVzdBo4LnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFByb2plY3RTdGF0dXNCeUlEUmVzcG9uc2UiABKOAQoVR2V0UHJvamVjdFN0YXR1c0J5VVJJEjgudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UHJvamVjdFN0YXR1c0J5VVJJUmVxdWVzdBo5LnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFByb2plY3RTdGF0dXNCeVVSSVJlc3BvbnNlIgASWwoETGlzdBInLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkxpc3RSZXF1ZXN0GigudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuTGlzdFJlc3BvbnNlIgASeQoOR2V0UXVldWVTdGF0dXMSMS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5HZXRRdWV1ZVN0YXR1c1JlcXVlc3QaMi51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5HZXRRdWV1ZVN0YXR1c1Jlc3BvbnNlIgBCZQoeY29tLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlQg5TdG92ZXBpcGVQcm90b1ABWjFnaXRodWIuY29tL3ViZXIvc3VibWl0cXVldWUvYXBpL3N0b3ZlcGlwZS9wcm90b3BiYgZwcm90bzM"); + fileDesc("CiNhcGkvc3RvdmVwaXBlL3Byb3RvL3N0b3ZlcGlwZS5wcm90bxIadWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUiHgoLUGluZ1JlcXVlc3QSDwoHbWVzc2FnZRgBIAEoCSJaCgxQaW5nUmVzcG9uc2USDwoHbWVzc2FnZRgBIAEoCRIUCgxzZXJ2aWNlX25hbWUYAiABKAkSEQoJdGltZXN0YW1wGAMgASgDEhAKCGhvc3RuYW1lGAQgASgJIl8KDUluZ2VzdFJlcXVlc3QSDQoFcXVldWUYASABKAkSPwoKZW5hYmxlbWVudBgCIAEoDjIrLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLlF1ZXVlRW5hYmxlbWVudCJjCg5Jbmdlc3RSZXNwb25zZRIKCgJpZBgBIAEoCRJFCgplbmFibGVtZW50GAIgASgLMjEudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUXVldWVFbmFibGVtZW50U3RhdHVzIkEKHEdldFJlcXVlc3RIaXN0b3J5QnlJRFJlcXVlc3QSDQoFcXVldWUYASABKAkSEgoKcmVxdWVzdF9pZBgCIAEoCSI7Ch1HZXRSZXF1ZXN0SGlzdG9yeUJ5VVJJUmVxdWVzdBINCgVxdWV1ZRgBIAEoCRILCgN1cmkYAiABKAkihgEKDEhpc3RvcnlFdmVudBIQCghldmVudF9pZBgBIAEoCRIUCgx0aW1lc3RhbXBfbXMYAiABKAMSFwoNcmVxdWVzdF9zdGF0ZRgDIAEoCUgAEg8KBWV2ZW50GAQgASgJSAASFgoOb3V0Y29tZV9yZWFzb24YBSABKAlCDAoKb2NjdXJyZW5jZSKjAQoOUmVxdWVzdEhpc3RvcnkSEgoKcmVxdWVzdF9pZBgBIAEoCRI4CgZldmVudHMYAiADKAsyKC51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5IaXN0b3J5RXZlbnQSQwoPcmVxdWVzdF9zdW1tYXJ5GAMgASgLMioudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUmVxdWVzdFN1bW1hcnkingEKHUdldFJlcXVlc3RIaXN0b3J5QnlJRFJlc3BvbnNlEjgKBmV2ZW50cxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkhpc3RvcnlFdmVudBJDCg9yZXF1ZXN0X3N1bW1hcnkYAiABKAsyKi51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5SZXF1ZXN0U3VtbWFyeSJfCh5HZXRSZXF1ZXN0SGlzdG9yeUJ5VVJJUmVzcG9uc2USPQoJaGlzdG9yaWVzGAEgAygLMioudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUmVxdWVzdEhpc3RvcnkieQobR2V0UHJvamVjdFN0YXR1c0J5SURSZXF1ZXN0Eg0KBXF1ZXVlGAEgASgJEhIKCnJlcXVlc3RfaWQYAiABKAkSEAoIcHJvamVjdHMYAyADKAkSEQoJcGFnZV9zaXplGAQgASgFEhIKCnBhZ2VfdG9rZW4YBSABKAkiegocR2V0UHJvamVjdFN0YXR1c0J5VVJJUmVxdWVzdBINCgVxdWV1ZRgBIAEoCRISCgpjaGFuZ2VfdXJpGAIgASgJEhAKCHByb2plY3RzGAMgAygJEhEKCXBhZ2Vfc2l6ZRgEIAEoBRISCgpwYWdlX3Rva2VuGAUgASgJIkkKEVByb2plY3RWYWxpZGF0aW9uEg8KB3Byb2plY3QYASABKAkSGQoPYnJlYWthZ2VfZGVncmVlGAIgASgBSABCCAoGcmVzdWx0IswCChxHZXRQcm9qZWN0U3RhdHVzQnlJRFJlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSDQoFcXVldWUYAiABKAkSEgoKY2hhbmdlX3VyaRgDIAEoCRIQCghiYXNlX3VyaRgEIAEoCRIVCg1yZXF1ZXN0X3N0YXRlGAUgASgJEhUKDXVwZGF0ZWRfYXRfbXMYCiABKAMSJAoacmVwb3NpdG9yeV9icmVha2FnZV9kZWdyZWUYBiABKAFIABIgChhwcm9qZWN0X3Jlc3VsdHNfY29tcGxldGUYByABKAgSPwoIcHJvamVjdHMYCCADKAsyLS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5Qcm9qZWN0VmFsaWRhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YCSABKAlCEwoRcmVwb3NpdG9yeV9yZXN1bHQizQIKHUdldFByb2plY3RTdGF0dXNCeVVSSVJlc3BvbnNlEhIKCnJlcXVlc3RfaWQYASABKAkSDQoFcXVldWUYAiABKAkSEgoKY2hhbmdlX3VyaRgDIAEoCRIQCghiYXNlX3VyaRgEIAEoCRIVCg1yZXF1ZXN0X3N0YXRlGAUgASgJEhUKDXVwZGF0ZWRfYXRfbXMYCiABKAMSJAoacmVwb3NpdG9yeV9icmVha2FnZV9kZWdyZWUYBiABKAFIABIgChhwcm9qZWN0X3Jlc3VsdHNfY29tcGxldGUYByABKAgSPwoIcHJvamVjdHMYCCADKAsyLS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5Qcm9qZWN0VmFsaWRhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YCSABKAlCEwoRcmVwb3NpdG9yeV9yZXN1bHQi8gEKDlJlcXVlc3RTdW1tYXJ5EhIKCnJlcXVlc3RfaWQYASABKAkSDQoFcXVldWUYAiABKAkSEgoKY2hhbmdlX3VyaRgDIAEoCRIQCghiYXNlX3VyaRgEIAEoCRIVCg1yZXF1ZXN0X3N0YXRlGAUgASgJEhsKE3N0YXRlX3VwZGF0ZWRfYXRfbXMYBiABKAMSFgoOYWNjZXB0ZWRfYXRfbXMYByABKAMSFgoOb3V0Y29tZV9yZWFzb24YCCABKAkSFQoNcHJvamVjdF9jb3VudBgJIAEoBRIcChRicm9rZW5fcHJvamVjdF9jb3VudBgKIAEoBSK0AQoLTGlzdFJlcXVlc3QSDQoFcXVldWUYASABKAkSIQoXYWNjZXB0ZWRfYXRfb3JfYWZ0ZXJfbXMYAiABKANIABIcChJhY2NlcHRlZF9iZWZvcmVfbXMYAyABKANIARIRCglwYWdlX3NpemUYBCABKAUSEgoKcGFnZV90b2tlbhgFIAEoCUIWChRhY2NlcHRlZF9sb3dlcl9ib3VuZEIWChRhY2NlcHRlZF91cHBlcl9ib3VuZCJlCgxMaXN0UmVzcG9uc2USPAoIcmVxdWVzdHMYASADKAsyKi51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5SZXF1ZXN0U3VtbWFyeRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkiOgoVR2V0UXVldWVTdGF0dXNSZXF1ZXN0Eg0KBXF1ZXVlGAEgASgJEhIKCmNoYW5nZV91cmkYAiABKAkingEKFVF1ZXVlRW5hYmxlbWVudFN0YXR1cxIPCgd2ZXJzaW9uGAEgASgDEjoKBXN0YXRlGAIgASgOMisudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuUXVldWVFbmFibGVtZW50EiEKGWVmZmVjdGl2ZV9mcm9tX2NvbW1pdF91cmkYAyABKAkSFQoNY2hhbmdlZF9hdF9tcxgEIAEoAyJuChZHZXRRdWV1ZVN0YXR1c1Jlc3BvbnNlEg0KBXF1ZXVlGAEgASgJEkUKCmVuYWJsZW1lbnQYAiABKAsyMS51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5RdWV1ZUVuYWJsZW1lbnRTdGF0dXMqbAoPUXVldWVFbmFibGVtZW50EhwKGFFVRVVFX0VOQUJMRU1FTlRfVU5LTk9XThAAEhwKGFFVRVVFX0VOQUJMRU1FTlRfRU5BQkxFRBABEh0KGVFVRVVFX0VOQUJMRU1FTlRfRElTQUJMRUQQAjLnBwoJU3RvdmVwaXBlElsKBFBpbmcSJy51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5QaW5nUmVxdWVzdBooLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLlBpbmdSZXNwb25zZSIAEmEKBkluZ2VzdBIpLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkluZ2VzdFJlcXVlc3QaKi51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5Jbmdlc3RSZXNwb25zZSIAEo4BChVHZXRSZXF1ZXN0SGlzdG9yeUJ5SUQSOC51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5HZXRSZXF1ZXN0SGlzdG9yeUJ5SURSZXF1ZXN0GjkudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UmVxdWVzdEhpc3RvcnlCeUlEUmVzcG9uc2UiABKRAQoWR2V0UmVxdWVzdEhpc3RvcnlCeVVSSRI5LnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFJlcXVlc3RIaXN0b3J5QnlVUklSZXF1ZXN0GjoudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UmVxdWVzdEhpc3RvcnlCeVVSSVJlc3BvbnNlIgASiwEKFEdldFByb2plY3RTdGF0dXNCeUlEEjcudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UHJvamVjdFN0YXR1c0J5SURSZXF1ZXN0GjgudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UHJvamVjdFN0YXR1c0J5SURSZXNwb25zZSIAEo4BChVHZXRQcm9qZWN0U3RhdHVzQnlVUkkSOC51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5HZXRQcm9qZWN0U3RhdHVzQnlVUklSZXF1ZXN0GjkudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuR2V0UHJvamVjdFN0YXR1c0J5VVJJUmVzcG9uc2UiABJbCgRMaXN0EicudWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGUuTGlzdFJlcXVlc3QaKC51YmVyLnN1Ym1pdHF1ZXVlLnN0b3ZlcGlwZS5MaXN0UmVzcG9uc2UiABJ5Cg5HZXRRdWV1ZVN0YXR1cxIxLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFF1ZXVlU3RhdHVzUmVxdWVzdBoyLnViZXIuc3VibWl0cXVldWUuc3RvdmVwaXBlLkdldFF1ZXVlU3RhdHVzUmVzcG9uc2UiAEJlCh5jb20udWJlci5zdWJtaXRxdWV1ZS5zdG92ZXBpcGVCDlN0b3ZlcGlwZVByb3RvUAFaMWdpdGh1Yi5jb20vdWJlci9zdWJtaXRxdWV1ZS9hcGkvc3RvdmVwaXBlL3Byb3RvcGJiBnByb3RvMw"); /** * PingRequest is the request for the Ping method @@ -719,6 +719,20 @@ export type RequestSummary = Message<"uber.submitqueue.stovepipe.RequestSummary" * @generated from field: string outcome_reason = 8; */ outcomeReason: string; + + /** + * Total recorded project results; zero when no project results have been recorded. + * + * @generated from field: int32 project_count = 9; + */ + projectCount: number; + + /** + * Recorded project results with degree above zero. + * + * @generated from field: int32 broken_project_count = 10; + */ + brokenProjectCount: number; }; /**