diff --git a/backend/api/v2beta1/experiment.proto b/backend/api/v2beta1/experiment.proto index 2222a385698..e111e4639a5 100644 --- a/backend/api/v2beta1/experiment.proto +++ b/backend/api/v2beta1/experiment.proto @@ -99,6 +99,9 @@ message Experiment { // Output. Specifies whether this experiment is in archived or available state. StorageState storage_state = 6; + + // Output. The creation time of the last run in this experiment. + google.protobuf.Timestamp last_run_created_at = 7; } message CreateExperimentRequest { diff --git a/backend/api/v2beta1/go_client/experiment.pb.go b/backend/api/v2beta1/go_client/experiment.pb.go index 0c7eec1a674..7d684008d7a 100644 --- a/backend/api/v2beta1/go_client/experiment.pb.go +++ b/backend/api/v2beta1/go_client/experiment.pb.go @@ -111,6 +111,8 @@ type Experiment struct { Namespace string `protobuf:"bytes,5,opt,name=namespace,proto3" json:"namespace,omitempty"` // Output. Specifies whether this experiment is in archived or available state. StorageState Experiment_StorageState `protobuf:"varint,6,opt,name=storage_state,json=storageState,proto3,enum=kubeflow.pipelines.backend.api.v2beta1.Experiment_StorageState" json:"storage_state,omitempty"` + // Output. The time the created time of the last run in this experiment. + LastRunCreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=last_run_created_at,json=lastRunCreatedAt,proto3" json:"last_run_created_at,omitempty"` } func (x *Experiment) Reset() { @@ -187,6 +189,13 @@ func (x *Experiment) GetStorageState() Experiment_StorageState { return Experiment_STORAGE_STATE_UNSPECIFIED } +func (x *Experiment) GetLastRunCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.LastRunCreatedAt + } + return nil +} + type CreateExperimentRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -596,7 +605,7 @@ var file_backend_api_v2beta1_experiment_proto_rawDesc = []byte{ 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x81, 0x03, 0x0a, 0x0a, 0x45, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcc, 0x03, 0x0a, 0x0a, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, @@ -616,128 +625,133 @@ var file_backend_api_v2beta1_experiment_proto_rawDesc = []byte{ 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x22, 0x4a, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x5f, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x01, - 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x52, 0x43, 0x48, 0x49, 0x56, 0x45, 0x44, 0x10, 0x02, 0x22, 0x6d, - 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x0a, 0x65, 0x78, 0x70, - 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, - 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, - 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x3b, 0x0a, - 0x14, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, - 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, - 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0xa3, 0x01, 0x0a, 0x16, 0x4c, - 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, - 0x65, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, - 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x22, 0xb6, 0x01, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0b, - 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, - 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, - 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, - 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, - 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x3e, 0x0a, 0x17, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x70, - 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x3f, 0x0a, 0x18, 0x41, 0x72, 0x63, - 0x68, 0x69, 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, - 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, - 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x41, 0x0a, 0x1a, 0x55, 0x6e, - 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, - 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x32, 0xb8, 0x08, - 0x0a, 0x11, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0xb6, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, - 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, + 0x74, 0x65, 0x12, 0x49, 0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, + 0x74, 0x52, 0x75, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x4a, 0x0a, + 0x0c, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, + 0x19, 0x53, 0x54, 0x4f, 0x52, 0x41, 0x47, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, + 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x41, + 0x52, 0x43, 0x48, 0x49, 0x56, 0x45, 0x44, 0x10, 0x02, 0x22, 0x6d, 0x0a, 0x17, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x52, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, - 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x6b, 0x75, 0x62, 0x65, - 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, - 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, - 0x61, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x2d, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x27, 0x22, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, - 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x3a, 0x0a, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0xb4, 0x01, 0x0a, - 0x0d, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3c, - 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, - 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, - 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x6b, + 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x65, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x3b, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x45, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0xa3, 0x01, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x17, 0x0a, 0x07, + 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xb6, 0x01, 0x0a, 0x17, + 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x65, 0x78, 0x70, 0x65, 0x72, + 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, - 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x12, 0x29, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, - 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, - 0x6e, 0x74, 0x73, 0x2f, 0x7b, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x7d, 0x12, 0xb5, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, - 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, - 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, - 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, + 0x52, 0x0b, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1d, 0x0a, + 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x26, 0x0a, 0x0f, + 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x3e, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, + 0x6e, 0x74, 0x49, 0x64, 0x22, 0x3f, 0x0a, 0x18, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, + 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x41, 0x0a, 0x1a, 0x55, 0x6e, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, + 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x32, 0xb8, 0x08, 0x0a, 0x11, 0x45, 0x78, 0x70, + 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xb6, + 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, + 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, + 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, + 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x45, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x2d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x27, + 0x22, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, + 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x3a, 0x0a, 0x65, 0x78, 0x70, + 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0xb4, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x45, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3c, 0x2e, 0x6b, 0x75, 0x62, 0x65, + 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, + 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, + 0x61, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, - 0x12, 0x19, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, - 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0xa8, 0x01, 0x0a, 0x11, - 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, - 0x74, 0x12, 0x40, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, - 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x41, 0x72, 0x63, 0x68, 0x69, - 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x39, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x33, 0x22, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, + 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x31, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x2b, 0x12, 0x29, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x7b, - 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, - 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0xae, 0x01, 0x0a, 0x13, 0x55, 0x6e, 0x61, 0x72, 0x63, - 0x68, 0x69, 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x42, - 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, - 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x55, 0x6e, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, - 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x3b, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x35, 0x22, 0x33, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, - 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x7b, 0x65, - 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x75, 0x6e, - 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x2e, 0x6b, + 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0xb5, + 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x12, 0x3e, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, + 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, + 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x21, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1b, 0x12, 0x19, 0x2f, 0x61, 0x70, + 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, + 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0xa8, 0x01, 0x0a, 0x11, 0x41, 0x72, 0x63, 0x68, 0x69, + 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x40, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, - 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, + 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, 0x78, 0x70, + 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x39, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x33, 0x22, 0x31, + 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, + 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x7b, 0x65, 0x78, 0x70, 0x65, 0x72, + 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, + 0x65, 0x12, 0xae, 0x01, 0x0a, 0x13, 0x55, 0x6e, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, + 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x42, 0x2e, 0x6b, 0x75, 0x62, 0x65, + 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, + 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, + 0x61, 0x31, 0x2e, 0x55, 0x6e, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x2a, 0x29, 0x2f, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x3b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x35, 0x22, 0x33, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x7b, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2f, - 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, - 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x67, 0x6f, - 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x75, 0x6e, 0x61, 0x72, 0x63, 0x68, 0x69, + 0x76, 0x65, 0x12, 0x9e, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x70, + 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3f, 0x2e, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, + 0x6f, 0x77, 0x2e, 0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x62, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x2a, 0x29, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, + 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2f, 0x7b, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x7d, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6b, 0x75, 0x62, 0x65, 0x66, 0x6c, 0x6f, 0x77, 0x2f, 0x70, 0x69, 0x70, 0x65, 0x6c, + 0x69, 0x6e, 0x65, 0x73, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x67, 0x6f, 0x5f, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -770,25 +784,26 @@ var file_backend_api_v2beta1_experiment_proto_goTypes = []interface{}{ var file_backend_api_v2beta1_experiment_proto_depIdxs = []int32{ 9, // 0: kubeflow.pipelines.backend.api.v2beta1.Experiment.created_at:type_name -> google.protobuf.Timestamp 0, // 1: kubeflow.pipelines.backend.api.v2beta1.Experiment.storage_state:type_name -> kubeflow.pipelines.backend.api.v2beta1.Experiment.StorageState - 1, // 2: kubeflow.pipelines.backend.api.v2beta1.CreateExperimentRequest.experiment:type_name -> kubeflow.pipelines.backend.api.v2beta1.Experiment - 1, // 3: kubeflow.pipelines.backend.api.v2beta1.ListExperimentsResponse.experiments:type_name -> kubeflow.pipelines.backend.api.v2beta1.Experiment - 2, // 4: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.CreateExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.CreateExperimentRequest - 3, // 5: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.GetExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.GetExperimentRequest - 4, // 6: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ListExperiments:input_type -> kubeflow.pipelines.backend.api.v2beta1.ListExperimentsRequest - 7, // 7: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ArchiveExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.ArchiveExperimentRequest - 8, // 8: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.UnarchiveExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.UnarchiveExperimentRequest - 6, // 9: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.DeleteExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.DeleteExperimentRequest - 1, // 10: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.CreateExperiment:output_type -> kubeflow.pipelines.backend.api.v2beta1.Experiment - 1, // 11: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.GetExperiment:output_type -> kubeflow.pipelines.backend.api.v2beta1.Experiment - 5, // 12: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ListExperiments:output_type -> kubeflow.pipelines.backend.api.v2beta1.ListExperimentsResponse - 10, // 13: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ArchiveExperiment:output_type -> google.protobuf.Empty - 10, // 14: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.UnarchiveExperiment:output_type -> google.protobuf.Empty - 10, // 15: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.DeleteExperiment:output_type -> google.protobuf.Empty - 10, // [10:16] is the sub-list for method output_type - 4, // [4:10] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 9, // 2: kubeflow.pipelines.backend.api.v2beta1.Experiment.last_run_created_at:type_name -> google.protobuf.Timestamp + 1, // 3: kubeflow.pipelines.backend.api.v2beta1.CreateExperimentRequest.experiment:type_name -> kubeflow.pipelines.backend.api.v2beta1.Experiment + 1, // 4: kubeflow.pipelines.backend.api.v2beta1.ListExperimentsResponse.experiments:type_name -> kubeflow.pipelines.backend.api.v2beta1.Experiment + 2, // 5: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.CreateExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.CreateExperimentRequest + 3, // 6: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.GetExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.GetExperimentRequest + 4, // 7: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ListExperiments:input_type -> kubeflow.pipelines.backend.api.v2beta1.ListExperimentsRequest + 7, // 8: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ArchiveExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.ArchiveExperimentRequest + 8, // 9: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.UnarchiveExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.UnarchiveExperimentRequest + 6, // 10: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.DeleteExperiment:input_type -> kubeflow.pipelines.backend.api.v2beta1.DeleteExperimentRequest + 1, // 11: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.CreateExperiment:output_type -> kubeflow.pipelines.backend.api.v2beta1.Experiment + 1, // 12: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.GetExperiment:output_type -> kubeflow.pipelines.backend.api.v2beta1.Experiment + 5, // 13: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ListExperiments:output_type -> kubeflow.pipelines.backend.api.v2beta1.ListExperimentsResponse + 10, // 14: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.ArchiveExperiment:output_type -> google.protobuf.Empty + 10, // 15: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.UnarchiveExperiment:output_type -> google.protobuf.Empty + 10, // 16: kubeflow.pipelines.backend.api.v2beta1.ExperimentService.DeleteExperiment:output_type -> google.protobuf.Empty + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_backend_api_v2beta1_experiment_proto_init() } diff --git a/backend/api/v2beta1/go_http_client/experiment_model/v2beta1_experiment.go b/backend/api/v2beta1/go_http_client/experiment_model/v2beta1_experiment.go index 6525c4160a7..e87c02f8e6e 100644 --- a/backend/api/v2beta1/go_http_client/experiment_model/v2beta1_experiment.go +++ b/backend/api/v2beta1/go_http_client/experiment_model/v2beta1_experiment.go @@ -30,6 +30,10 @@ type V2beta1Experiment struct { // Output. Unique experiment ID. Generated by API server. ExperimentID string `json:"experiment_id,omitempty"` + // Output. The time the created time of the last run in this experiment. + // Format: date-time + LastRunCreatedAt strfmt.DateTime `json:"last_run_created_at,omitempty"` + // Optional input field. Specify the namespace this experiment belongs to. Namespace string `json:"namespace,omitempty"` @@ -45,6 +49,10 @@ func (m *V2beta1Experiment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateLastRunCreatedAt(formats); err != nil { + res = append(res, err) + } + if err := m.validateStorageState(formats); err != nil { res = append(res, err) } @@ -68,6 +76,19 @@ func (m *V2beta1Experiment) validateCreatedAt(formats strfmt.Registry) error { return nil } +func (m *V2beta1Experiment) validateLastRunCreatedAt(formats strfmt.Registry) error { + + if swag.IsZero(m.LastRunCreatedAt) { // not required + return nil + } + + if err := validate.FormatOf("last_run_created_at", "body", "date-time", m.LastRunCreatedAt.String(), formats); err != nil { + return err + } + + return nil +} + func (m *V2beta1Experiment) validateStorageState(formats strfmt.Registry) error { if swag.IsZero(m.StorageState) { // not required diff --git a/backend/api/v2beta1/swagger/experiment.swagger.json b/backend/api/v2beta1/swagger/experiment.swagger.json index 8be40c2e108..18248768ac8 100644 --- a/backend/api/v2beta1/swagger/experiment.swagger.json +++ b/backend/api/v2beta1/swagger/experiment.swagger.json @@ -227,6 +227,11 @@ "storage_state": { "$ref": "#/definitions/v2beta1ExperimentStorageState", "description": "Output. Specifies whether this experiment is in archived or available state." + }, + "last_run_created_at": { + "type": "string", + "format": "date-time", + "description": "Output. The time the created time of the last run in this experiment." } } }, diff --git a/backend/api/v2beta1/swagger/kfp_api_single_file.swagger.json b/backend/api/v2beta1/swagger/kfp_api_single_file.swagger.json index 8f3e5ee04e0..77b6cb3fd33 100644 --- a/backend/api/v2beta1/swagger/kfp_api_single_file.swagger.json +++ b/backend/api/v2beta1/swagger/kfp_api_single_file.swagger.json @@ -1523,6 +1523,11 @@ "storage_state": { "$ref": "#/definitions/v2beta1ExperimentStorageState", "description": "Output. Specifies whether this experiment is in archived or available state." + }, + "last_run_created_at": { + "type": "string", + "format": "date-time", + "description": "Output. The time the created time of the last run in this experiment." } } }, diff --git a/backend/src/apiserver/model/experiment.go b/backend/src/apiserver/model/experiment.go index 950ca66e11b..0adde8e8f42 100644 --- a/backend/src/apiserver/model/experiment.go +++ b/backend/src/apiserver/model/experiment.go @@ -15,12 +15,13 @@ package model type Experiment struct { - UUID string `gorm:"column:UUID; not null; primary_key;"` - Name string `gorm:"column:Name; not null; unique_index:idx_name_namespace;"` - Description string `gorm:"column:Description; not null;"` - CreatedAtInSec int64 `gorm:"column:CreatedAtInSec; not null;"` - Namespace string `gorm:"column:Namespace; not null; unique_index:idx_name_namespace;"` - StorageState StorageState `gorm:"column:StorageState; not null;"` + UUID string `gorm:"column:UUID; not null; primary_key;"` + Name string `gorm:"column:Name; not null; unique_index:idx_name_namespace;"` + Description string `gorm:"column:Description; not null;"` + CreatedAtInSec int64 `gorm:"column:CreatedAtInSec; not null;"` + LastRunCreatedAtInSec int64 `gorm:"column:LastRunCreatedAtInSec; not null;"` + Namespace string `gorm:"column:Namespace; not null; unique_index:idx_name_namespace;"` + StorageState StorageState `gorm:"column:StorageState; not null;"` } // Note: Experiment.StorageState can have values: "STORAGE_STATE_UNSPECIFIED", "AVAILABLE" or "ARCHIVED" @@ -44,14 +45,15 @@ func (e *Experiment) DefaultSortField() string { } var experimentAPIToModelFieldMap = map[string]string{ - "id": "UUID", // v1beta1 API - "experiment_id": "UUID", // v2beta1 API - "name": "Name", // v1beta1 API - "display_name": "Name", // v2beta1 API - "created_at": "CreatedAtInSec", - "description": "Description", - "namespace": "Namespace", // v2beta1 API - "storage_state": "StorageState", + "id": "UUID", // v1beta1 API + "experiment_id": "UUID", // v2beta1 API + "name": "Name", // v1beta1 API + "display_name": "Name", // v2beta1 API + "created_at": "CreatedAtInSec", + "last_run_created_at": "LastRunCreatedAtInSec", // v2beta1 API + "description": "Description", + "namespace": "Namespace", // v2beta1 API + "storage_state": "StorageState", } // APIToModelFieldMap returns a map from API names to field names for model @@ -80,6 +82,8 @@ func (e *Experiment) GetFieldValue(name string) interface{} { return e.Name case "CreatedAtInSec": return e.CreatedAtInSec + case "LastRunCreatedAtInSec": + return e.LastRunCreatedAtInSec case "Description": return e.Description case "Namespace": diff --git a/backend/src/apiserver/resource/resource_manager.go b/backend/src/apiserver/resource/resource_manager.go index 422e053b0ff..758ab5b0524 100644 --- a/backend/src/apiserver/resource/resource_manager.go +++ b/backend/src/apiserver/resource/resource_manager.go @@ -550,6 +550,13 @@ func (r *ResourceManager) CreateRun(ctx context.Context, run *model.Run) (*model if err != nil { return nil, util.Wrap(err, "Failed to create a run") } + + // Upon run creation, update owning experiment + err = r.experimentStore.UpdateLastRun(newRun) + if err != nil { + return nil, util.Wrap(err, fmt.Sprintf("Failed to update last_run_created_at in experiment %s for run %s", newRun.ExperimentId, newRun.UUID)) + } + return newRun, nil } @@ -1256,6 +1263,10 @@ func (r *ResourceManager) ReportWorkflowResource(ctx context.Context, execSpec u } else { runId = run.UUID } + // Upon run creation, update owning experiment + if updateError = r.experimentStore.UpdateLastRun(run); updateError != nil { + return nil, util.Wrapf(updateError, "Failed to report a workflow for existing run %s during updating the owning experiment.", runId) + } } if execStatus.IsInFinalState() { err := addWorkflowLabel(ctx, r.getWorkflowClient(execSpec.ExecutionNamespace()), execSpec.ExecutionName(), util.LabelKeyWorkflowPersistedFinalState, "true") diff --git a/backend/src/apiserver/server/api_converter.go b/backend/src/apiserver/server/api_converter.go index 8ac760edb1d..9cb07771c38 100644 --- a/backend/src/apiserver/server/api_converter.go +++ b/backend/src/apiserver/server/api_converter.go @@ -113,12 +113,13 @@ func toApiExperiment(experiment *model.Experiment) *apiv2beta1.Experiment { storageState = apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["STORAGE_STATE_UNSPECIFIED"]) } return &apiv2beta1.Experiment{ - ExperimentId: experiment.UUID, - DisplayName: experiment.Name, - Description: experiment.Description, - CreatedAt: ×tamp.Timestamp{Seconds: experiment.CreatedAtInSec}, - Namespace: experiment.Namespace, - StorageState: storageState, + ExperimentId: experiment.UUID, + DisplayName: experiment.Name, + Description: experiment.Description, + CreatedAt: ×tamp.Timestamp{Seconds: experiment.CreatedAtInSec}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: experiment.LastRunCreatedAtInSec}, + Namespace: experiment.Namespace, + StorageState: storageState, } } diff --git a/backend/src/apiserver/server/api_converter_test.go b/backend/src/apiserver/server/api_converter_test.go index 3b7d052fc91..b7b204e913d 100644 --- a/backend/src/apiserver/server/api_converter_test.go +++ b/backend/src/apiserver/server/api_converter_test.go @@ -1974,77 +1974,87 @@ func TestToApiExperimentsV1(t *testing.T) { func TestToApiExperiments(t *testing.T) { exp1 := &model.Experiment{ - UUID: "exp1", - CreatedAtInSec: 1, - Name: "experiment1", - Description: "My name is experiment1", - StorageState: "AVAILABLE", + UUID: "exp1", + CreatedAtInSec: 1, + LastRunCreatedAtInSec: 1, + Name: "experiment1", + Description: "My name is experiment1", + StorageState: "AVAILABLE", } exp2 := &model.Experiment{ - UUID: "exp2", - CreatedAtInSec: 2, - Name: "experiment2", - Description: "My name is experiment2", - StorageState: "ARCHIVED", + UUID: "exp2", + CreatedAtInSec: 2, + LastRunCreatedAtInSec: 2, + Name: "experiment2", + Description: "My name is experiment2", + StorageState: "ARCHIVED", } exp3 := &model.Experiment{ - UUID: "exp3", - CreatedAtInSec: 1, - Name: "experiment3", - Description: "experiment3 was created using V1 APIV1BETA1", - StorageState: "STORAGESTATE_AVAILABLE", + UUID: "exp3", + CreatedAtInSec: 1, + LastRunCreatedAtInSec: 1, + Name: "experiment3", + Description: "experiment3 was created using V1 APIV1BETA1", + StorageState: "STORAGESTATE_AVAILABLE", } exp4 := &model.Experiment{ - UUID: "exp4", - CreatedAtInSec: 2, - Name: "experiment4", - Description: "experiment4 was created using V1 APIV1BETA1", - StorageState: "STORAGESTATE_ARCHIVED", + UUID: "exp4", + CreatedAtInSec: 2, + LastRunCreatedAtInSec: 2, + Name: "experiment4", + Description: "experiment4 was created using V1 APIV1BETA1", + StorageState: "STORAGESTATE_ARCHIVED", } exp5 := &model.Experiment{ - UUID: "exp5", - CreatedAtInSec: 1, - Name: "experiment5", - Description: "My name is experiment5", - StorageState: "this is invalid storage state", + UUID: "exp5", + CreatedAtInSec: 1, + LastRunCreatedAtInSec: 1, + Name: "experiment5", + Description: "My name is experiment5", + StorageState: "this is invalid storage state", } apiExps := toApiExperiments([]*model.Experiment{exp1, exp2, exp3, exp4, nil, exp5}) expectedApiExps := []*apiv2beta1.Experiment{ { - ExperimentId: "exp1", - DisplayName: "experiment1", - Description: "My name is experiment1", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["AVAILABLE"]), + ExperimentId: "exp1", + DisplayName: "experiment1", + Description: "My name is experiment1", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 1}, + StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["AVAILABLE"]), }, { - ExperimentId: "exp2", - DisplayName: "experiment2", - Description: "My name is experiment2", - CreatedAt: ×tamp.Timestamp{Seconds: 2}, - StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["ARCHIVED"]), + ExperimentId: "exp2", + DisplayName: "experiment2", + Description: "My name is experiment2", + CreatedAt: ×tamp.Timestamp{Seconds: 2}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 2}, + StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["ARCHIVED"]), }, { - ExperimentId: "exp3", - DisplayName: "experiment3", - Description: "experiment3 was created using V1 APIV1BETA1", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["AVAILABLE"]), + ExperimentId: "exp3", + DisplayName: "experiment3", + Description: "experiment3 was created using V1 APIV1BETA1", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 1}, + StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["AVAILABLE"]), }, { - ExperimentId: "exp4", - DisplayName: "experiment4", - Description: "experiment4 was created using V1 APIV1BETA1", - CreatedAt: ×tamp.Timestamp{Seconds: 2}, - StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["ARCHIVED"]), + ExperimentId: "exp4", + DisplayName: "experiment4", + Description: "experiment4 was created using V1 APIV1BETA1", + CreatedAt: ×tamp.Timestamp{Seconds: 2}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 2}, + StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["ARCHIVED"]), }, {}, { - ExperimentId: "exp5", - DisplayName: "experiment5", - Description: "My name is experiment5", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["STORAGE_STATE_UNSPECIFIED"]), + ExperimentId: "exp5", + DisplayName: "experiment5", + Description: "My name is experiment5", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 1}, + StorageState: apiv2beta1.Experiment_StorageState(apiv2beta1.Experiment_StorageState_value["STORAGE_STATE_UNSPECIFIED"]), }, } assert.Equal(t, expectedApiExps, apiExps) diff --git a/backend/src/apiserver/server/experiment_server_test.go b/backend/src/apiserver/server/experiment_server_test.go index 44a1dc2d042..cdea5babcbd 100644 --- a/backend/src/apiserver/server/experiment_server_test.go +++ b/backend/src/apiserver/server/experiment_server_test.go @@ -16,6 +16,8 @@ package server import ( "context" + "google.golang.org/protobuf/types/known/structpb" + "sigs.k8s.io/yaml" "strings" "testing" @@ -66,12 +68,13 @@ func TestCreateExperiment(t *testing.T) { result, err := server.CreateExperiment(nil, &apiV2beta1.CreateExperimentRequest{Experiment: experiment}) assert.Nil(t, err) expectedExperiment := &apiV2beta1.Experiment{ - ExperimentId: DefaultFakeUUID, - DisplayName: "ex1", - Description: "first experiment", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - StorageState: apiV2beta1.Experiment_AVAILABLE, - Namespace: "", + ExperimentId: DefaultFakeUUID, + DisplayName: "ex1", + Description: "first experiment", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + StorageState: apiV2beta1.Experiment_AVAILABLE, + Namespace: "", } assert.Equal(t, expectedExperiment, result) } @@ -395,16 +398,18 @@ func TestCreateExperiment_Multiuser(t *testing.T) { { "Valid", &apiV2beta1.Experiment{ - DisplayName: "exp1", - Description: "first experiment", - Namespace: "ns1", + DisplayName: "exp1", + Description: "first experiment", + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + Namespace: "ns1", }, &apiV2beta1.Experiment{ - ExperimentId: DefaultFakeUUID, - DisplayName: "exp1", - Description: "first experiment", - Namespace: "ns1", - StorageState: apiV2beta1.Experiment_AVAILABLE, + ExperimentId: DefaultFakeUUID, + DisplayName: "exp1", + Description: "first experiment", + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + Namespace: "ns1", + StorageState: apiV2beta1.Experiment_AVAILABLE, }, false, "", @@ -481,12 +486,13 @@ func TestGetExperiment(t *testing.T) { result, err := server.GetExperiment(nil, &apiV2beta1.GetExperimentRequest{ExperimentId: createResult.ExperimentId}) assert.Nil(t, err) expectedExperiment := &apiV2beta1.Experiment{ - ExperimentId: createResult.ExperimentId, - DisplayName: "ex1", - Description: "first experiment", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - StorageState: apiV2beta1.Experiment_AVAILABLE, - Namespace: "", + ExperimentId: createResult.ExperimentId, + DisplayName: "ex1", + Description: "first experiment", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + StorageState: apiV2beta1.Experiment_AVAILABLE, + Namespace: "", } assert.Equal(t, expectedExperiment, result) } @@ -619,12 +625,13 @@ func TestGetExperiment_Multiuser(t *testing.T) { result, err := server.GetExperiment(ctx, &apiV2beta1.GetExperimentRequest{ExperimentId: createResult.ExperimentId}) assert.Nil(t, err) expectedExperiment := &apiV2beta1.Experiment{ - ExperimentId: createResult.ExperimentId, - DisplayName: "exp1", - Description: "first experiment", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - Namespace: "ns1", - StorageState: apiV2beta1.Experiment_AVAILABLE, + ExperimentId: createResult.ExperimentId, + DisplayName: "exp1", + Description: "first experiment", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + Namespace: "ns1", + StorageState: apiV2beta1.Experiment_AVAILABLE, } assert.Equal(t, expectedExperiment, result) } @@ -667,17 +674,97 @@ func TestListExperiments(t *testing.T) { assert.Nil(t, err) result, err := server.ListExperiments(nil, &apiV2beta1.ListExperimentsRequest{}) expectedExperiment := []*apiV2beta1.Experiment{{ - ExperimentId: createResult.ExperimentId, - DisplayName: "ex1", - Description: "first experiment", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - StorageState: apiV2beta1.Experiment_AVAILABLE, - Namespace: "", + ExperimentId: createResult.ExperimentId, + DisplayName: "ex1", + Description: "first experiment", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + StorageState: apiV2beta1.Experiment_AVAILABLE, + Namespace: "", }} assert.Nil(t, err) assert.Equal(t, expectedExperiment, result.Experiments) } +func TestListExperimentsByLastRunCreation(t *testing.T) { + // Create experiment and runs/jobs under it. + clients, manager, experiment1, _ := initWithExperimentAndPipelineVersion(t) + defer clients.Close() + + // Create another experiment + clients.UpdateUUID(util.NewFakeUUIDGeneratorOrFatal(DefaultFakeIdTwo, nil)) + manager = resource.NewResourceManager(clients, &resource.ResourceManagerOptions{CollectMetrics: false}) + server := ExperimentServer{resourceManager: manager, options: &ExperimentServerOptions{CollectMetrics: false}} + experiment := &apiV2beta1.Experiment{DisplayName: "exp2"} + experiment2, err := server.CreateExperiment(nil, &apiV2beta1.CreateExperimentRequest{Experiment: experiment}) + assert.Nil(t, err) + + // Create a generic run object + pipelineSpecStruct := &structpb.Struct{} + yaml.Unmarshal([]byte(v2SpecHelloWorld), pipelineSpecStruct) + genericRun := &apiV2beta1.Run{ + PipelineSource: &apiV2beta1.Run_PipelineSpec{ + PipelineSpec: pipelineSpecStruct, + }, + RuntimeConfig: &apiV2beta1.RuntimeConfig{ + Parameters: map[string]*structpb.Value{ + "param1": structpb.NewStringValue("world"), + }, + }, + } + + // Create a run in experiment 1 + clients.UpdateUUID(util.NewFakeUUIDGeneratorOrFatal(DefaultFakeIdThree, nil)) + manager = resource.NewResourceManager(clients, &resource.ResourceManagerOptions{CollectMetrics: false}) + runServer := NewRunServer(manager, &RunServerOptions{CollectMetrics: false}) + genericRun.DisplayName = "run1" + genericRun.ExperimentId = experiment1.UUID + _, err = runServer.CreateRun(nil, &apiV2beta1.CreateRunRequest{Run: genericRun}) + assert.Nil(t, err) + + // Create a run in experiment 2 + clients.UpdateUUID(util.NewFakeUUIDGeneratorOrFatal(DefaultFakeIdFour, nil)) + manager = resource.NewResourceManager(clients, &resource.ResourceManagerOptions{CollectMetrics: false}) + runServer = NewRunServer(manager, &RunServerOptions{CollectMetrics: false}) + genericRun.DisplayName = "run2" + genericRun.ExperimentId = experiment2.ExperimentId + _, err = runServer.CreateRun(nil, &apiV2beta1.CreateRunRequest{Run: genericRun}) + assert.Nil(t, err) + + // Expected runs, note that because run 2 in experiment 2 + // was created last, experiment 2 has the latest run execution + experimentServer := ExperimentServer{resourceManager: manager, options: &ExperimentServerOptions{CollectMetrics: false}} + expected1 := &apiV2beta1.Experiment{ + ExperimentId: experiment1.UUID, + DisplayName: "exp1", + Description: "", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 6}, + StorageState: apiV2beta1.Experiment_AVAILABLE, + Namespace: "", + } + expected2 := &apiV2beta1.Experiment{ + ExperimentId: experiment2.ExperimentId, + DisplayName: "exp2", + Description: "", + CreatedAt: ×tamp.Timestamp{Seconds: 5}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 8}, + StorageState: apiV2beta1.Experiment_AVAILABLE, + Namespace: "", + } + + // First list runs sorted by last_run_created_at ascending + listExperimentsRequest := &apiV2beta1.ListExperimentsRequest{SortBy: "last_run_created_at asc"} + result, err := experimentServer.ListExperiments(nil, listExperimentsRequest) + assert.Nil(t, err) + assert.Equal(t, []*apiV2beta1.Experiment{expected1, expected2}, result.Experiments) + + // Then list runs sorted by last_run_created_at descending, note the order is switched + listExperimentsRequest = &apiV2beta1.ListExperimentsRequest{SortBy: "last_run_created_at desc"} + result, err = experimentServer.ListExperiments(nil, listExperimentsRequest) + assert.Equal(t, []*apiV2beta1.Experiment{expected2, expected1}, result.Experiments) +} + func TestListExperimentsV1_Failed(t *testing.T) { clientManager := resource.NewFakeClientManagerOrFatal(util.NewFakeTimeForEpoch()) resourceManager := resource.NewResourceManager(clientManager, &resource.ResourceManagerOptions{CollectMetrics: false}) @@ -913,12 +1000,13 @@ func TestListExperiments_Multiuser_NoDefault(t *testing.T) { false, "", []*apiV2beta1.Experiment{{ - ExperimentId: createResult.ExperimentId, - DisplayName: "exp1", - Description: "first experiment", - CreatedAt: ×tamp.Timestamp{Seconds: 1}, - Namespace: "ns1", - StorageState: apiV2beta1.Experiment_AVAILABLE, + ExperimentId: createResult.ExperimentId, + DisplayName: "exp1", + Description: "first experiment", + CreatedAt: ×tamp.Timestamp{Seconds: 1}, + LastRunCreatedAt: ×tamp.Timestamp{Seconds: 0}, + Namespace: "ns1", + StorageState: apiV2beta1.Experiment_AVAILABLE, }}, }, { diff --git a/backend/src/apiserver/storage/experiment_store.go b/backend/src/apiserver/storage/experiment_store.go index febfa8b2d05..7681d88a0ff 100644 --- a/backend/src/apiserver/storage/experiment_store.go +++ b/backend/src/apiserver/storage/experiment_store.go @@ -33,6 +33,7 @@ type ExperimentStoreInterface interface { ArchiveExperiment(expId string) error UnarchiveExperiment(expId string) error DeleteExperiment(uuid string) error + UpdateLastRun(run *model.Run) error } type ExperimentStore struct { @@ -48,6 +49,7 @@ var experimentColumns = []string{ "Name", "Description", "CreatedAtInSec", + "LastRunCreatedAtInSec", "Namespace", "StorageState", } @@ -195,17 +197,19 @@ func (s *ExperimentStore) scanRows(rows *sql.Rows) ([]*model.Experiment, error) for rows.Next() { var uuid, name, description, namespace, storageState string var createdAtInSec sql.NullInt64 - err := rows.Scan(&uuid, &name, &description, &createdAtInSec, &namespace, &storageState) + var lastRunCreatedAtInSec sql.NullInt64 + err := rows.Scan(&uuid, &name, &description, &createdAtInSec, &lastRunCreatedAtInSec, &namespace, &storageState) if err != nil { return experiments, err } experiment := &model.Experiment{ - UUID: uuid, - Name: name, - Description: description, - CreatedAtInSec: createdAtInSec.Int64, - Namespace: namespace, - StorageState: model.StorageState(storageState).ToV2(), + UUID: uuid, + Name: name, + Description: description, + CreatedAtInSec: createdAtInSec.Int64, + LastRunCreatedAtInSec: lastRunCreatedAtInSec.Int64, + Namespace: namespace, + StorageState: model.StorageState(storageState).ToV2(), } // Since storage state is a field added after initial KFP release, it is possible that existing experiments don't have this field and we use AVAILABLE in that case. if experiment.StorageState == "" || experiment.StorageState == model.StorageStateUnspecified { @@ -220,6 +224,9 @@ func (s *ExperimentStore) CreateExperiment(experiment *model.Experiment) (*model newExperiment := *experiment now := s.time.Now().Unix() newExperiment.CreatedAtInSec = now + // When an experiment has no runs + // we default to "1970-01-01T00:00:00Z" + newExperiment.LastRunCreatedAtInSec = 0 id, err := s.uuid.NewRandom() if err != nil { return nil, util.NewInternalServerError(err, "Failed to create an experiment id") @@ -236,12 +243,13 @@ func (s *ExperimentStore) CreateExperiment(experiment *model.Experiment) (*model sql, args, err := sq. Insert("experiments"). SetMap(sq.Eq{ - "UUID": newExperiment.UUID, - "CreatedAtInSec": newExperiment.CreatedAtInSec, - "Name": newExperiment.Name, - "Description": newExperiment.Description, - "Namespace": newExperiment.Namespace, - "StorageState": newExperiment.StorageState.ToV2().ToString(), + "UUID": newExperiment.UUID, + "CreatedAtInSec": newExperiment.CreatedAtInSec, + "LastRunCreatedAtInSec": newExperiment.LastRunCreatedAtInSec, + "Name": newExperiment.Name, + "Description": newExperiment.Description, + "Namespace": newExperiment.Namespace, + "StorageState": newExperiment.StorageState.ToV2().ToString(), }). ToSql() if err != nil { @@ -411,6 +419,28 @@ func (s *ExperimentStore) UnarchiveExperiment(expId string) error { return nil } +func (s *ExperimentStore) UpdateLastRun(run *model.Run) error { + expId := run.ExperimentId + // UpdateLastRun results in the experiment getting last_run_created_at updated + query, args, err := sq. + Update("experiments"). + SetMap(sq.Eq{ + "LastRunCreatedAtInSec": run.CreatedAtInSec, + }). + Where(sq.Eq{"UUID": expId}). + ToSql() + if err != nil { + return util.NewInternalServerError(err, + "Failed to create query to set experiment LastRunCreatedAtInSec %s. error: '%v'", expId, err.Error()) + } + _, err = s.db.Exec(query, args...) + if err != nil { + return util.NewInternalServerError(err, + "Failed to set experiment LastRunCreatedAtInSec %s. error: '%v'", expId, err.Error()) + } + return nil +} + // factory function for experiment store. func NewExperimentStore(db *DB, time util.TimeInterface, uuid util.UUIDGeneratorInterface) *ExperimentStore { return &ExperimentStore{ diff --git a/backend/src/apiserver/storage/experiment_store_test.go b/backend/src/apiserver/storage/experiment_store_test.go index 291a1816f50..366df98b244 100644 --- a/backend/src/apiserver/storage/experiment_store_test.go +++ b/backend/src/apiserver/storage/experiment_store_test.go @@ -60,18 +60,20 @@ func TestListExperiments_Pagination(t *testing.T) { experimentStore.uuid = util.NewFakeUUIDGeneratorOrFatal(fakeIDFour, nil) experimentStore.CreateExperiment(createExperiment("experiment2")) expectedExperiment1 := &model.Experiment{ - UUID: fakeID, - CreatedAtInSec: 1, - Name: "experiment1", - Description: "My name is experiment1", - StorageState: "AVAILABLE", + UUID: fakeID, + CreatedAtInSec: 1, + LastRunCreatedAtInSec: 0, + Name: "experiment1", + Description: "My name is experiment1", + StorageState: "AVAILABLE", } expectedExperiment4 := &model.Experiment{ - UUID: fakeIDFour, - CreatedAtInSec: 4, - Name: "experiment2", - Description: "My name is experiment2", - StorageState: "AVAILABLE", + UUID: fakeIDFour, + CreatedAtInSec: 4, + LastRunCreatedAtInSec: 0, + Name: "experiment2", + Description: "My name is experiment2", + StorageState: "AVAILABLE", } experimentsExpected := []*model.Experiment{expectedExperiment1, expectedExperiment4} opts, err := list.NewOptions(&model.Experiment{}, 2, "name", nil) @@ -85,18 +87,20 @@ func TestListExperiments_Pagination(t *testing.T) { assert.Equal(t, 4, total_size) expectedExperiment2 := &model.Experiment{ - UUID: fakeIDTwo, - CreatedAtInSec: 2, - Name: "experiment3", - Description: "My name is experiment3", - StorageState: "AVAILABLE", + UUID: fakeIDTwo, + CreatedAtInSec: 2, + LastRunCreatedAtInSec: 0, + Name: "experiment3", + Description: "My name is experiment3", + StorageState: "AVAILABLE", } expectedExperiment3 := &model.Experiment{ - UUID: fakeIDThree, - CreatedAtInSec: 3, - Name: "experiment4", - Description: "My name is experiment4", - StorageState: "AVAILABLE", + UUID: fakeIDThree, + CreatedAtInSec: 3, + LastRunCreatedAtInSec: 0, + Name: "experiment4", + Description: "My name is experiment4", + StorageState: "AVAILABLE", } experimentsExpected2 := []*model.Experiment{expectedExperiment2, expectedExperiment3}