diff --git a/.gitignore b/.gitignore index 198dcdd879..ee19fe6a64 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ jobs /resources/resource_fs.go __pycache__ .optimus.yaml +coverage.txt # git ignore generate files related to gRPC and proto /proton/ diff --git a/api/handler/v1/runtime.go b/api/handler/v1/runtime.go index d56e1fe50c..611afcee2a 100644 --- a/api/handler/v1/runtime.go +++ b/api/handler/v1/runtime.go @@ -727,7 +727,44 @@ func (sv *RuntimeServiceServer) ListResourceSpecification(ctx context.Context, r }, nil } -func (sv *RuntimeServiceServer) ReplayDryRun(ctx context.Context, req *pb.ReplayDryRunRequest) (*pb.ReplayDryRunResponse, error) { +func (sv *RuntimeServiceServer) ReplayDryRun(ctx context.Context, req *pb.ReplayRequest) (*pb.ReplayDryRunResponse, error) { + replayWorkerRequest, err := sv.parseReplayRequest(req) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("error while parsing replay dry run request: %v", err)) + } + + rootNode, err := sv.jobSvc.ReplayDryRun(replayWorkerRequest) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("error while processing replay dry run: %v", err)) + } + + node, err := sv.adapter.ToReplayExecutionTreeNode(rootNode) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("error while preparing replay dry run response: %v", err)) + } + return &pb.ReplayDryRunResponse{ + Success: true, + Response: node, + }, nil +} + +func (sv *RuntimeServiceServer) Replay(ctx context.Context, req *pb.ReplayRequest) (*pb.ReplayResponse, error) { + replayWorkerRequest, err := sv.parseReplayRequest(req) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("error while parsing replay request: %v", err)) + } + + replayUUID, err := sv.jobSvc.Replay(replayWorkerRequest) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("error while processing replay: %v", err)) + } + + return &pb.ReplayResponse{ + Id: replayUUID, + }, nil +} + +func (sv *RuntimeServiceServer) parseReplayRequest(req *pb.ReplayRequest) (*models.ReplayWorkerRequest, error) { projectRepo := sv.projectRepoFactory.New() projSpec, err := projectRepo.GetByName(req.GetProjectName()) if err != nil { @@ -760,20 +797,13 @@ func (sv *RuntimeServiceServer) ReplayDryRun(ctx context.Context, req *pb.Replay if endDate.Before(startDate) { return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("replay end date cannot be before start date")) } - - rootNode, err := sv.jobSvc.ReplayDryRun(namespaceSpec, jobSpec, startDate, endDate) - if err != nil { - return nil, status.Error(codes.Internal, fmt.Sprintf("error while processing replay: %v", err)) - } - - node, err := sv.adapter.ToReplayExecutionTreeNode(rootNode) - if err != nil { - return nil, status.Error(codes.Internal, fmt.Sprintf("error while processing replay: %v", err)) + replayRequest := models.ReplayWorkerRequest{ + Job: jobSpec, + Start: startDate, + End: endDate, + Project: projSpec, } - return &pb.ReplayDryRunResponse{ - Success: true, - Response: node, - }, nil + return &replayRequest, nil } func NewRuntimeServiceServer( diff --git a/api/handler/v1/runtime_test.go b/api/handler/v1/runtime_test.go index 5e20259192..859cf3cf18 100644 --- a/api/handler/v1/runtime_test.go +++ b/api/handler/v1/runtime_test.go @@ -1479,11 +1479,17 @@ func TestRuntimeServiceServer(t *testing.T) { }, }), } + replayWorkerRequest := &models.ReplayWorkerRequest{ + Job: jobSpec, + Start: startDate, + End: endDate, + Project: projectSpec, + } dagNode := tree.NewTreeNode(jobSpec) jobService := new(mock.JobService) jobService.On("GetByName", jobName, namespaceSpec).Return(jobSpec, nil) - jobService.On("ReplayDryRun", namespaceSpec, jobSpec, startDate, endDate).Return(dagNode, nil) + jobService.On("ReplayDryRun", replayWorkerRequest).Return(dagNode, nil) defer jobService.AssertExpectations(t) projectRepository := new(mock.ProjectRepository) @@ -1514,7 +1520,7 @@ func TestRuntimeServiceServer(t *testing.T) { nil, nil, ) - replayRequest := pb.ReplayDryRunRequest{ + replayRequest := pb.ReplayRequest{ ProjectName: projectName, Namespace: namespaceSpec.Name, JobName: jobName, diff --git a/api/proto/odpf/optimus/runtime_service.pb.go b/api/proto/odpf/optimus/runtime_service.pb.go index 4c7ff7d5d1..a355c92c5e 100644 --- a/api/proto/odpf/optimus/runtime_service.pb.go +++ b/api/proto/odpf/optimus/runtime_service.pb.go @@ -3529,7 +3529,7 @@ func (x *UpdateResourceResponse) GetMessage() string { return "" } -type ReplayDryRunRequest struct { +type ReplayRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -3541,8 +3541,8 @@ type ReplayDryRunRequest struct { EndDate string `protobuf:"bytes,5,opt,name=end_date,json=endDate,proto3" json:"end_date,omitempty"` } -func (x *ReplayDryRunRequest) Reset() { - *x = ReplayDryRunRequest{} +func (x *ReplayRequest) Reset() { + *x = ReplayRequest{} if protoimpl.UnsafeEnabled { mi := &file_odpf_optimus_runtime_service_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3550,13 +3550,13 @@ func (x *ReplayDryRunRequest) Reset() { } } -func (x *ReplayDryRunRequest) String() string { +func (x *ReplayRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ReplayDryRunRequest) ProtoMessage() {} +func (*ReplayRequest) ProtoMessage() {} -func (x *ReplayDryRunRequest) ProtoReflect() protoreflect.Message { +func (x *ReplayRequest) ProtoReflect() protoreflect.Message { mi := &file_odpf_optimus_runtime_service_proto_msgTypes[55] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3568,40 +3568,40 @@ func (x *ReplayDryRunRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ReplayDryRunRequest.ProtoReflect.Descriptor instead. -func (*ReplayDryRunRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ReplayRequest.ProtoReflect.Descriptor instead. +func (*ReplayRequest) Descriptor() ([]byte, []int) { return file_odpf_optimus_runtime_service_proto_rawDescGZIP(), []int{55} } -func (x *ReplayDryRunRequest) GetProjectName() string { +func (x *ReplayRequest) GetProjectName() string { if x != nil { return x.ProjectName } return "" } -func (x *ReplayDryRunRequest) GetJobName() string { +func (x *ReplayRequest) GetJobName() string { if x != nil { return x.JobName } return "" } -func (x *ReplayDryRunRequest) GetNamespace() string { +func (x *ReplayRequest) GetNamespace() string { if x != nil { return x.Namespace } return "" } -func (x *ReplayDryRunRequest) GetStartDate() string { +func (x *ReplayRequest) GetStartDate() string { if x != nil { return x.StartDate } return "" } -func (x *ReplayDryRunRequest) GetEndDate() string { +func (x *ReplayRequest) GetEndDate() string { if x != nil { return x.EndDate } @@ -3726,6 +3726,53 @@ func (x *ReplayExecutionTreeNode) GetRuns() []*timestamp.Timestamp { return nil } +type ReplayResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *ReplayResponse) Reset() { + *x = ReplayResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ReplayResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReplayResponse) ProtoMessage() {} + +func (x *ReplayResponse) ProtoReflect() protoreflect.Message { + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[58] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReplayResponse.ProtoReflect.Descriptor instead. +func (*ReplayResponse) Descriptor() ([]byte, []int) { + return file_odpf_optimus_runtime_service_proto_rawDescGZIP(), []int{58} +} + +func (x *ReplayResponse) GetId() string { + if x != nil { + return x.Id + } + return "" +} + type ProjectSpecification_ProjectSecret struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3738,7 +3785,7 @@ type ProjectSpecification_ProjectSecret struct { func (x *ProjectSpecification_ProjectSecret) Reset() { *x = ProjectSpecification_ProjectSecret{} if protoimpl.UnsafeEnabled { - mi := &file_odpf_optimus_runtime_service_proto_msgTypes[59] + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3751,7 +3798,7 @@ func (x *ProjectSpecification_ProjectSecret) String() string { func (*ProjectSpecification_ProjectSecret) ProtoMessage() {} func (x *ProjectSpecification_ProjectSecret) ProtoReflect() protoreflect.Message { - mi := &file_odpf_optimus_runtime_service_proto_msgTypes[59] + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[60] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3792,7 +3839,7 @@ type JobSpecification_Behavior struct { func (x *JobSpecification_Behavior) Reset() { *x = JobSpecification_Behavior{} if protoimpl.UnsafeEnabled { - mi := &file_odpf_optimus_runtime_service_proto_msgTypes[63] + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3805,7 +3852,7 @@ func (x *JobSpecification_Behavior) String() string { func (*JobSpecification_Behavior) ProtoMessage() {} func (x *JobSpecification_Behavior) ProtoReflect() protoreflect.Message { - mi := &file_odpf_optimus_runtime_service_proto_msgTypes[63] + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[64] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3842,7 +3889,7 @@ type JobSpecification_Behavior_Retry struct { func (x *JobSpecification_Behavior_Retry) Reset() { *x = JobSpecification_Behavior_Retry{} if protoimpl.UnsafeEnabled { - mi := &file_odpf_optimus_runtime_service_proto_msgTypes[64] + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3855,7 +3902,7 @@ func (x *JobSpecification_Behavior_Retry) String() string { func (*JobSpecification_Behavior_Retry) ProtoMessage() {} func (x *JobSpecification_Behavior_Retry) ProtoReflect() protoreflect.Message { - mi := &file_odpf_optimus_runtime_service_proto_msgTypes[64] + mi := &file_odpf_optimus_runtime_service_proto_msgTypes[65] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4432,274 +4479,284 @@ var file_odpf_optimus_runtime_service_proto_rawDesc = []byte{ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x52, 0x65, - 0x70, 0x6c, 0x61, 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x44, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, - 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x65, 0x6e, 0x64, 0x44, 0x61, 0x74, 0x65, 0x22, 0x73, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x61, - 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x41, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x64, - 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, + 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x0d, 0x52, 0x65, + 0x70, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, + 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x44, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x61, + 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x44, 0x61, 0x74, + 0x65, 0x22, 0x73, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x12, 0x41, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, + 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x45, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x08, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, - 0x64, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xab, 0x01, 0x0a, - 0x17, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, - 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x45, 0x78, 0x65, - 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x0a, - 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x75, - 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 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, 0x04, 0x72, 0x75, 0x6e, 0x73, 0x32, 0xe0, 0x1c, 0x0a, 0x0e, 0x52, - 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x62, 0x0a, - 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, - 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, 0x0f, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3a, 0x01, - 0x2a, 0x12, 0x77, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4a, 0x6f, 0x62, 0x53, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x6f, 0x64, - 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, - 0x79, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4a, 0x6f, + 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x45, 0x0a, + 0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, + 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x72, 0x65, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x75, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, + 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, 0x04, + 0x72, 0x75, 0x6e, 0x73, 0x22, 0x20, 0x0a, 0x0e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x32, 0xde, 0x1d, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x74, 0x69, + 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x62, 0x0a, 0x07, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, + 0x6d, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, + 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3a, 0x01, 0x2a, 0x12, 0x77, 0x0a, + 0x16, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, + 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4a, 0x6f, 0x62, + 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, + 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, + 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0xb8, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, + 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x43, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x3d, 0x22, 0x38, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x3a, 0x01, + 0x2a, 0x12, 0xba, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x61, 0x64, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, + 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x6f, 0x64, 0x70, + 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0xb8, 0x01, 0x0a, 0x16, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, - 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, - 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x43, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3d, 0x22, 0x38, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x6a, - 0x6f, 0x62, 0x3a, 0x01, 0x2a, 0x12, 0xba, 0x01, 0x0a, 0x14, 0x52, 0x65, 0x61, 0x64, 0x4a, 0x6f, - 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, - 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, - 0x61, 0x64, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, - 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x4a, 0x6f, 0x62, - 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x45, 0x12, 0x43, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x7d, 0x12, 0xc0, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, - 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, - 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, - 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x45, - 0x2a, 0x43, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, - 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x99, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4a, 0x6f, - 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, - 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, - 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4a, 0x6f, 0x62, - 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x12, 0x22, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, - 0x62, 0x12, 0xa9, 0x01, 0x0a, 0x14, 0x44, 0x75, 0x6d, 0x70, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, + 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x4b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x45, 0x12, 0x43, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, + 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0xc0, + 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, + 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4a, + 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, + 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4a, 0x6f, 0x62, 0x53, + 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x45, 0x2a, 0x43, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x7d, 0x12, 0x99, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x6f, 0x64, 0x70, - 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x4a, 0x6f, + 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, - 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, + 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x3a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x34, 0x12, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x12, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, - 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x75, 0x6d, 0x70, 0x12, 0xa2, 0x01, - 0x0a, 0x15, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, - 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, - 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, + 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x12, 0xa9, 0x01, + 0x0a, 0x14, 0x44, 0x75, 0x6d, 0x70, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, + 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, + 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, + 0x2e, 0x44, 0x75, 0x6d, 0x70, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3a, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x34, 0x12, 0x32, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x75, 0x6d, 0x70, 0x12, 0xa2, 0x01, 0x0a, 0x15, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x30, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2a, 0x22, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, - 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x12, 0x77, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, - 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x2e, 0x6f, - 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, 0x66, - 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, - 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x7a, 0x0a, 0x0f, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x24, - 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, - 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x14, 0x22, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x3a, 0x01, 0x2a, 0x12, 0xae, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x2d, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, - 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, - 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x33, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, 0x22, 0x28, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x9b, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x23, 0x2e, 0x6f, 0x64, - 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x24, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x38, 0x22, 0x33, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, - 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x7b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x7d, 0x3a, 0x01, 0x2a, 0x12, 0x6e, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x21, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, - 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6f, 0x64, 0x70, 0x66, - 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x11, 0x12, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0xa2, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x50, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, - 0x12, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x6f, - 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x2a, 0x22, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x77, + 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, + 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, + 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, + 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4a, 0x6f, 0x62, 0x53, 0x70, 0x65, + 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x7a, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x24, 0x2e, 0x6f, 0x64, 0x70, + 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x25, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x22, + 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x3a, 0x01, 0x2a, 0x12, 0xae, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x2a, 0x12, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0xa4, 0x01, 0x0a, 0x10, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x12, 0x25, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, - 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x41, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3b, 0x22, 0x36, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, + 0x12, 0x2d, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2e, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x33, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, 0x22, 0x28, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x3a, - 0x01, 0x2a, 0x12, 0x8a, 0x01, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x1e, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, - 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, - 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x3c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x36, 0x12, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, - 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x64, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x1e, 0x2e, 0x6f, - 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x57, - 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6f, - 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x57, - 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x77, - 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x86, 0x01, 0x0a, 0x1b, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, - 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, - 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0xde, - 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x2e, 0x6f, - 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6f, + 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x9b, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x23, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, + 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6f, + 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x3e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x38, 0x22, 0x33, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2f, 0x7b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x3a, + 0x01, 0x2a, 0x12, 0x6e, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x12, 0x21, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, + 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, + 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x17, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x11, 0x12, 0x0f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x12, 0xa2, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x2a, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x60, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x5a, 0x12, 0x58, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, + 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x30, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2a, 0x12, 0x28, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0xa4, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x25, 0x2e, 0x6f, + 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, + 0x75, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x3b, 0x22, 0x36, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x7d, 0x2f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x8a, + 0x01, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x2e, 0x6f, + 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4a, 0x6f, 0x62, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6f, + 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4a, 0x6f, 0x62, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3c, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x36, 0x12, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, - 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0xc0, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x23, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, - 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, - 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x63, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x5d, 0x22, 0x58, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x64, 0x0a, 0x09, 0x47, + 0x65, 0x74, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x1e, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, + 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x57, 0x69, 0x6e, 0x64, 0x6f, + 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, + 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x57, 0x69, 0x6e, 0x64, 0x6f, + 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x10, 0x12, 0x0e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x77, 0x69, 0x6e, 0x64, 0x6f, + 0x77, 0x12, 0x86, 0x01, 0x0a, 0x1b, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x30, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, + 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, + 0x75, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0xde, 0x01, 0x0a, 0x19, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, + 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, + 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x60, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x5a, 0x12, 0x58, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0xc0, 0x01, 0x0a, 0x0e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x23, + 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, + 0x75, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x63, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x5d, 0x22, 0x58, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0xc7, + 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x21, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, + 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x70, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x6a, 0x12, 0x68, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, + 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x7b, + 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x7b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0xc0, 0x01, 0x0a, 0x0e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x23, 0x2e, 0x6f, 0x64, + 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x24, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x63, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x5d, 0x1a, 0x58, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, + 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x7b, + 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3a, 0x01, 0x2a, 0x12, 0x95, 0x01, 0x0a, 0x0c, + 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x12, 0x1b, 0x2e, 0x6f, + 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, + 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6f, 0x64, 0x70, 0x66, + 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x44, + 0x72, 0x79, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x44, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x3e, 0x12, 0x3c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, - 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3a, - 0x01, 0x2a, 0x12, 0xc7, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x21, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, - 0x75, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, - 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x70, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x6a, 0x12, 0x68, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x7b, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0xc0, 0x01, 0x0a, - 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x23, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, - 0x6d, 0x75, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x63, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x5d, 0x1a, 0x58, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x7d, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2f, 0x7b, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x7d, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, - 0x72, 0x65, 0x2f, 0x7b, 0x64, 0x61, 0x74, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3a, 0x01, 0x2a, 0x12, - 0x9b, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, - 0x12, 0x21, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, - 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, - 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x44, 0x72, 0x79, 0x52, 0x75, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x44, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3e, 0x12, - 0x3c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, - 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x72, - 0x65, 0x70, 0x6c, 0x61, 0x79, 0x2d, 0x64, 0x72, 0x79, 0x2d, 0x72, 0x75, 0x6e, 0x42, 0x70, 0x0a, - 0x16, 0x69, 0x6f, 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2e, - 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x42, 0x15, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x01, - 0x5a, 0x1e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x64, 0x70, - 0x66, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, - 0x92, 0x41, 0x1c, 0x12, 0x05, 0x32, 0x03, 0x30, 0x2e, 0x31, 0x2a, 0x01, 0x01, 0x72, 0x10, 0x0a, - 0x0e, 0x4f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x2d, 0x64, 0x72, 0x79, 0x2d, + 0x72, 0x75, 0x6e, 0x12, 0x81, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x12, 0x1b, + 0x2e, 0x6f, 0x64, 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, + 0x70, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6f, 0x64, + 0x70, 0x66, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3c, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x36, 0x22, 0x34, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x7d, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x7d, + 0x2f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x42, 0x70, 0x0a, 0x16, 0x69, 0x6f, 0x2e, 0x6f, 0x64, + 0x70, 0x66, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2e, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, + 0x73, 0x42, 0x15, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x01, 0x5a, 0x1e, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x64, 0x70, 0x66, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x6e, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6d, 0x75, 0x73, 0x92, 0x41, 0x1c, 0x12, 0x05, 0x32, + 0x03, 0x30, 0x2e, 0x31, 0x2a, 0x01, 0x01, 0x72, 0x10, 0x0a, 0x0e, 0x4f, 0x70, 0x74, 0x69, 0x6d, + 0x75, 0x73, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -4715,7 +4772,7 @@ func file_odpf_optimus_runtime_service_proto_rawDescGZIP() []byte { } var file_odpf_optimus_runtime_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_odpf_optimus_runtime_service_proto_msgTypes = make([]protoimpl.MessageInfo, 69) +var file_odpf_optimus_runtime_service_proto_msgTypes = make([]protoimpl.MessageInfo, 70) var file_odpf_optimus_runtime_service_proto_goTypes = []interface{}{ (InstanceSpec_Type)(0), // 0: odpf.optimus.InstanceSpec.Type (InstanceSpecData_Type)(0), // 1: odpf.optimus.InstanceSpecData.Type @@ -4774,44 +4831,45 @@ var file_odpf_optimus_runtime_service_proto_goTypes = []interface{}{ (*ReadResourceResponse)(nil), // 54: odpf.optimus.ReadResourceResponse (*UpdateResourceRequest)(nil), // 55: odpf.optimus.UpdateResourceRequest (*UpdateResourceResponse)(nil), // 56: odpf.optimus.UpdateResourceResponse - (*ReplayDryRunRequest)(nil), // 57: odpf.optimus.ReplayDryRunRequest + (*ReplayRequest)(nil), // 57: odpf.optimus.ReplayRequest (*ReplayDryRunResponse)(nil), // 58: odpf.optimus.ReplayDryRunResponse (*ReplayExecutionTreeNode)(nil), // 59: odpf.optimus.ReplayExecutionTreeNode - nil, // 60: odpf.optimus.ProjectSpecification.ConfigEntry - (*ProjectSpecification_ProjectSecret)(nil), // 61: odpf.optimus.ProjectSpecification.ProjectSecret - nil, // 62: odpf.optimus.NamespaceSpecification.ConfigEntry - nil, // 63: odpf.optimus.JobSpecification.AssetsEntry - nil, // 64: odpf.optimus.JobSpecification.LabelsEntry - (*JobSpecification_Behavior)(nil), // 65: odpf.optimus.JobSpecification.Behavior - (*JobSpecification_Behavior_Retry)(nil), // 66: odpf.optimus.JobSpecification.Behavior.Retry - nil, // 67: odpf.optimus.InstanceContext.EnvsEntry - nil, // 68: odpf.optimus.InstanceContext.FilesEntry - nil, // 69: odpf.optimus.ResourceSpecification.AssetsEntry - nil, // 70: odpf.optimus.ResourceSpecification.LabelsEntry - (*timestamp.Timestamp)(nil), // 71: google.protobuf.Timestamp - (*_struct.Struct)(nil), // 72: google.protobuf.Struct - (*duration.Duration)(nil), // 73: google.protobuf.Duration + (*ReplayResponse)(nil), // 60: odpf.optimus.ReplayResponse + nil, // 61: odpf.optimus.ProjectSpecification.ConfigEntry + (*ProjectSpecification_ProjectSecret)(nil), // 62: odpf.optimus.ProjectSpecification.ProjectSecret + nil, // 63: odpf.optimus.NamespaceSpecification.ConfigEntry + nil, // 64: odpf.optimus.JobSpecification.AssetsEntry + nil, // 65: odpf.optimus.JobSpecification.LabelsEntry + (*JobSpecification_Behavior)(nil), // 66: odpf.optimus.JobSpecification.Behavior + (*JobSpecification_Behavior_Retry)(nil), // 67: odpf.optimus.JobSpecification.Behavior.Retry + nil, // 68: odpf.optimus.InstanceContext.EnvsEntry + nil, // 69: odpf.optimus.InstanceContext.FilesEntry + nil, // 70: odpf.optimus.ResourceSpecification.AssetsEntry + nil, // 71: odpf.optimus.ResourceSpecification.LabelsEntry + (*timestamp.Timestamp)(nil), // 72: google.protobuf.Timestamp + (*_struct.Struct)(nil), // 73: google.protobuf.Struct + (*duration.Duration)(nil), // 74: google.protobuf.Duration } var file_odpf_optimus_runtime_service_proto_depIdxs = []int32{ - 60, // 0: odpf.optimus.ProjectSpecification.config:type_name -> odpf.optimus.ProjectSpecification.ConfigEntry - 61, // 1: odpf.optimus.ProjectSpecification.secrets:type_name -> odpf.optimus.ProjectSpecification.ProjectSecret - 62, // 2: odpf.optimus.NamespaceSpecification.config:type_name -> odpf.optimus.NamespaceSpecification.ConfigEntry + 61, // 0: odpf.optimus.ProjectSpecification.config:type_name -> odpf.optimus.ProjectSpecification.ConfigEntry + 62, // 1: odpf.optimus.ProjectSpecification.secrets:type_name -> odpf.optimus.ProjectSpecification.ProjectSecret + 63, // 2: odpf.optimus.NamespaceSpecification.config:type_name -> odpf.optimus.NamespaceSpecification.ConfigEntry 6, // 3: odpf.optimus.JobSpecHook.config:type_name -> odpf.optimus.JobConfigItem 6, // 4: odpf.optimus.JobSpecification.config:type_name -> odpf.optimus.JobConfigItem 7, // 5: odpf.optimus.JobSpecification.dependencies:type_name -> odpf.optimus.JobDependency - 63, // 6: odpf.optimus.JobSpecification.assets:type_name -> odpf.optimus.JobSpecification.AssetsEntry + 64, // 6: odpf.optimus.JobSpecification.assets:type_name -> odpf.optimus.JobSpecification.AssetsEntry 4, // 7: odpf.optimus.JobSpecification.hooks:type_name -> odpf.optimus.JobSpecHook - 64, // 8: odpf.optimus.JobSpecification.labels:type_name -> odpf.optimus.JobSpecification.LabelsEntry - 65, // 9: odpf.optimus.JobSpecification.behavior:type_name -> odpf.optimus.JobSpecification.Behavior - 71, // 10: odpf.optimus.InstanceSpec.scheduled_at:type_name -> google.protobuf.Timestamp + 65, // 8: odpf.optimus.JobSpecification.labels:type_name -> odpf.optimus.JobSpecification.LabelsEntry + 66, // 9: odpf.optimus.JobSpecification.behavior:type_name -> odpf.optimus.JobSpecification.Behavior + 72, // 10: odpf.optimus.InstanceSpec.scheduled_at:type_name -> google.protobuf.Timestamp 9, // 11: odpf.optimus.InstanceSpec.data:type_name -> odpf.optimus.InstanceSpecData 1, // 12: odpf.optimus.InstanceSpecData.type:type_name -> odpf.optimus.InstanceSpecData.Type - 67, // 13: odpf.optimus.InstanceContext.envs:type_name -> odpf.optimus.InstanceContext.EnvsEntry - 68, // 14: odpf.optimus.InstanceContext.files:type_name -> odpf.optimus.InstanceContext.FilesEntry - 71, // 15: odpf.optimus.JobStatus.scheduled_at:type_name -> google.protobuf.Timestamp - 72, // 16: odpf.optimus.ResourceSpecification.spec:type_name -> google.protobuf.Struct - 69, // 17: odpf.optimus.ResourceSpecification.assets:type_name -> odpf.optimus.ResourceSpecification.AssetsEntry - 70, // 18: odpf.optimus.ResourceSpecification.labels:type_name -> odpf.optimus.ResourceSpecification.LabelsEntry + 68, // 13: odpf.optimus.InstanceContext.envs:type_name -> odpf.optimus.InstanceContext.EnvsEntry + 69, // 14: odpf.optimus.InstanceContext.files:type_name -> odpf.optimus.InstanceContext.FilesEntry + 72, // 15: odpf.optimus.JobStatus.scheduled_at:type_name -> google.protobuf.Timestamp + 73, // 16: odpf.optimus.ResourceSpecification.spec:type_name -> google.protobuf.Struct + 70, // 17: odpf.optimus.ResourceSpecification.assets:type_name -> odpf.optimus.ResourceSpecification.AssetsEntry + 71, // 18: odpf.optimus.ResourceSpecification.labels:type_name -> odpf.optimus.ResourceSpecification.LabelsEntry 5, // 19: odpf.optimus.DeployJobSpecificationRequest.jobs:type_name -> odpf.optimus.JobSpecification 5, // 20: odpf.optimus.ListJobSpecificationResponse.jobs:type_name -> odpf.optimus.JobSpecification 5, // 21: odpf.optimus.CheckJobSpecificationRequest.job:type_name -> odpf.optimus.JobSpecification @@ -4823,7 +4881,7 @@ var file_odpf_optimus_runtime_service_proto_depIdxs = []int32{ 5, // 27: odpf.optimus.ReadJobSpecificationResponse.spec:type_name -> odpf.optimus.JobSpecification 2, // 28: odpf.optimus.ListProjectsResponse.projects:type_name -> odpf.optimus.ProjectSpecification 3, // 29: odpf.optimus.ListProjectNamespacesResponse.namespaces:type_name -> odpf.optimus.NamespaceSpecification - 71, // 30: odpf.optimus.RegisterInstanceRequest.scheduled_at:type_name -> google.protobuf.Timestamp + 72, // 30: odpf.optimus.RegisterInstanceRequest.scheduled_at:type_name -> google.protobuf.Timestamp 0, // 31: odpf.optimus.RegisterInstanceRequest.instance_type:type_name -> odpf.optimus.InstanceSpec.Type 2, // 32: odpf.optimus.RegisterInstanceResponse.project:type_name -> odpf.optimus.ProjectSpecification 5, // 33: odpf.optimus.RegisterInstanceResponse.job:type_name -> odpf.optimus.JobSpecification @@ -4831,9 +4889,9 @@ var file_odpf_optimus_runtime_service_proto_depIdxs = []int32{ 3, // 35: odpf.optimus.RegisterInstanceResponse.namespace:type_name -> odpf.optimus.NamespaceSpecification 10, // 36: odpf.optimus.RegisterInstanceResponse.context:type_name -> odpf.optimus.InstanceContext 11, // 37: odpf.optimus.JobStatusResponse.statuses:type_name -> odpf.optimus.JobStatus - 71, // 38: odpf.optimus.GetWindowRequest.scheduled_at:type_name -> google.protobuf.Timestamp - 71, // 39: odpf.optimus.GetWindowResponse.start:type_name -> google.protobuf.Timestamp - 71, // 40: odpf.optimus.GetWindowResponse.end:type_name -> google.protobuf.Timestamp + 72, // 38: odpf.optimus.GetWindowRequest.scheduled_at:type_name -> google.protobuf.Timestamp + 72, // 39: odpf.optimus.GetWindowResponse.start:type_name -> google.protobuf.Timestamp + 72, // 40: odpf.optimus.GetWindowResponse.end:type_name -> google.protobuf.Timestamp 12, // 41: odpf.optimus.DeployResourceSpecificationRequest.resources:type_name -> odpf.optimus.ResourceSpecification 12, // 42: odpf.optimus.ListResourceSpecificationResponse.resources:type_name -> odpf.optimus.ResourceSpecification 12, // 43: odpf.optimus.CreateResourceRequest.resource:type_name -> odpf.optimus.ResourceSpecification @@ -4841,9 +4899,9 @@ var file_odpf_optimus_runtime_service_proto_depIdxs = []int32{ 12, // 45: odpf.optimus.UpdateResourceRequest.resource:type_name -> odpf.optimus.ResourceSpecification 59, // 46: odpf.optimus.ReplayDryRunResponse.response:type_name -> odpf.optimus.ReplayExecutionTreeNode 59, // 47: odpf.optimus.ReplayExecutionTreeNode.dependents:type_name -> odpf.optimus.ReplayExecutionTreeNode - 71, // 48: odpf.optimus.ReplayExecutionTreeNode.runs:type_name -> google.protobuf.Timestamp - 66, // 49: odpf.optimus.JobSpecification.Behavior.retry:type_name -> odpf.optimus.JobSpecification.Behavior.Retry - 73, // 50: odpf.optimus.JobSpecification.Behavior.Retry.delay:type_name -> google.protobuf.Duration + 72, // 48: odpf.optimus.ReplayExecutionTreeNode.runs:type_name -> google.protobuf.Timestamp + 67, // 49: odpf.optimus.JobSpecification.Behavior.retry:type_name -> odpf.optimus.JobSpecification.Behavior.Retry + 74, // 50: odpf.optimus.JobSpecification.Behavior.Retry.delay:type_name -> google.protobuf.Duration 13, // 51: odpf.optimus.RuntimeService.Version:input_type -> odpf.optimus.VersionRequest 15, // 52: odpf.optimus.RuntimeService.DeployJobSpecification:input_type -> odpf.optimus.DeployJobSpecificationRequest 29, // 53: odpf.optimus.RuntimeService.CreateJobSpecification:input_type -> odpf.optimus.CreateJobSpecificationRequest @@ -4866,32 +4924,34 @@ var file_odpf_optimus_runtime_service_proto_depIdxs = []int32{ 51, // 70: odpf.optimus.RuntimeService.CreateResource:input_type -> odpf.optimus.CreateResourceRequest 53, // 71: odpf.optimus.RuntimeService.ReadResource:input_type -> odpf.optimus.ReadResourceRequest 55, // 72: odpf.optimus.RuntimeService.UpdateResource:input_type -> odpf.optimus.UpdateResourceRequest - 57, // 73: odpf.optimus.RuntimeService.ReplayDryRun:input_type -> odpf.optimus.ReplayDryRunRequest - 14, // 74: odpf.optimus.RuntimeService.Version:output_type -> odpf.optimus.VersionResponse - 16, // 75: odpf.optimus.RuntimeService.DeployJobSpecification:output_type -> odpf.optimus.DeployJobSpecificationResponse - 30, // 76: odpf.optimus.RuntimeService.CreateJobSpecification:output_type -> odpf.optimus.CreateJobSpecificationResponse - 32, // 77: odpf.optimus.RuntimeService.ReadJobSpecification:output_type -> odpf.optimus.ReadJobSpecificationResponse - 34, // 78: odpf.optimus.RuntimeService.DeleteJobSpecification:output_type -> odpf.optimus.DeleteJobSpecificationResponse - 18, // 79: odpf.optimus.RuntimeService.ListJobSpecification:output_type -> odpf.optimus.ListJobSpecificationResponse - 20, // 80: odpf.optimus.RuntimeService.DumpJobSpecification:output_type -> odpf.optimus.DumpJobSpecificationResponse - 22, // 81: odpf.optimus.RuntimeService.CheckJobSpecification:output_type -> odpf.optimus.CheckJobSpecificationResponse - 24, // 82: odpf.optimus.RuntimeService.CheckJobSpecifications:output_type -> odpf.optimus.CheckJobSpecificationsResponse - 26, // 83: odpf.optimus.RuntimeService.RegisterProject:output_type -> odpf.optimus.RegisterProjectResponse - 28, // 84: odpf.optimus.RuntimeService.RegisterProjectNamespace:output_type -> odpf.optimus.RegisterProjectNamespaceResponse - 36, // 85: odpf.optimus.RuntimeService.RegisterSecret:output_type -> odpf.optimus.RegisterSecretResponse - 38, // 86: odpf.optimus.RuntimeService.ListProjects:output_type -> odpf.optimus.ListProjectsResponse - 40, // 87: odpf.optimus.RuntimeService.ListProjectNamespaces:output_type -> odpf.optimus.ListProjectNamespacesResponse - 42, // 88: odpf.optimus.RuntimeService.RegisterInstance:output_type -> odpf.optimus.RegisterInstanceResponse - 44, // 89: odpf.optimus.RuntimeService.JobStatus:output_type -> odpf.optimus.JobStatusResponse - 46, // 90: odpf.optimus.RuntimeService.GetWindow:output_type -> odpf.optimus.GetWindowResponse - 48, // 91: odpf.optimus.RuntimeService.DeployResourceSpecification:output_type -> odpf.optimus.DeployResourceSpecificationResponse - 50, // 92: odpf.optimus.RuntimeService.ListResourceSpecification:output_type -> odpf.optimus.ListResourceSpecificationResponse - 52, // 93: odpf.optimus.RuntimeService.CreateResource:output_type -> odpf.optimus.CreateResourceResponse - 54, // 94: odpf.optimus.RuntimeService.ReadResource:output_type -> odpf.optimus.ReadResourceResponse - 56, // 95: odpf.optimus.RuntimeService.UpdateResource:output_type -> odpf.optimus.UpdateResourceResponse - 58, // 96: odpf.optimus.RuntimeService.ReplayDryRun:output_type -> odpf.optimus.ReplayDryRunResponse - 74, // [74:97] is the sub-list for method output_type - 51, // [51:74] is the sub-list for method input_type + 57, // 73: odpf.optimus.RuntimeService.ReplayDryRun:input_type -> odpf.optimus.ReplayRequest + 57, // 74: odpf.optimus.RuntimeService.Replay:input_type -> odpf.optimus.ReplayRequest + 14, // 75: odpf.optimus.RuntimeService.Version:output_type -> odpf.optimus.VersionResponse + 16, // 76: odpf.optimus.RuntimeService.DeployJobSpecification:output_type -> odpf.optimus.DeployJobSpecificationResponse + 30, // 77: odpf.optimus.RuntimeService.CreateJobSpecification:output_type -> odpf.optimus.CreateJobSpecificationResponse + 32, // 78: odpf.optimus.RuntimeService.ReadJobSpecification:output_type -> odpf.optimus.ReadJobSpecificationResponse + 34, // 79: odpf.optimus.RuntimeService.DeleteJobSpecification:output_type -> odpf.optimus.DeleteJobSpecificationResponse + 18, // 80: odpf.optimus.RuntimeService.ListJobSpecification:output_type -> odpf.optimus.ListJobSpecificationResponse + 20, // 81: odpf.optimus.RuntimeService.DumpJobSpecification:output_type -> odpf.optimus.DumpJobSpecificationResponse + 22, // 82: odpf.optimus.RuntimeService.CheckJobSpecification:output_type -> odpf.optimus.CheckJobSpecificationResponse + 24, // 83: odpf.optimus.RuntimeService.CheckJobSpecifications:output_type -> odpf.optimus.CheckJobSpecificationsResponse + 26, // 84: odpf.optimus.RuntimeService.RegisterProject:output_type -> odpf.optimus.RegisterProjectResponse + 28, // 85: odpf.optimus.RuntimeService.RegisterProjectNamespace:output_type -> odpf.optimus.RegisterProjectNamespaceResponse + 36, // 86: odpf.optimus.RuntimeService.RegisterSecret:output_type -> odpf.optimus.RegisterSecretResponse + 38, // 87: odpf.optimus.RuntimeService.ListProjects:output_type -> odpf.optimus.ListProjectsResponse + 40, // 88: odpf.optimus.RuntimeService.ListProjectNamespaces:output_type -> odpf.optimus.ListProjectNamespacesResponse + 42, // 89: odpf.optimus.RuntimeService.RegisterInstance:output_type -> odpf.optimus.RegisterInstanceResponse + 44, // 90: odpf.optimus.RuntimeService.JobStatus:output_type -> odpf.optimus.JobStatusResponse + 46, // 91: odpf.optimus.RuntimeService.GetWindow:output_type -> odpf.optimus.GetWindowResponse + 48, // 92: odpf.optimus.RuntimeService.DeployResourceSpecification:output_type -> odpf.optimus.DeployResourceSpecificationResponse + 50, // 93: odpf.optimus.RuntimeService.ListResourceSpecification:output_type -> odpf.optimus.ListResourceSpecificationResponse + 52, // 94: odpf.optimus.RuntimeService.CreateResource:output_type -> odpf.optimus.CreateResourceResponse + 54, // 95: odpf.optimus.RuntimeService.ReadResource:output_type -> odpf.optimus.ReadResourceResponse + 56, // 96: odpf.optimus.RuntimeService.UpdateResource:output_type -> odpf.optimus.UpdateResourceResponse + 58, // 97: odpf.optimus.RuntimeService.ReplayDryRun:output_type -> odpf.optimus.ReplayDryRunResponse + 60, // 98: odpf.optimus.RuntimeService.Replay:output_type -> odpf.optimus.ReplayResponse + 75, // [75:99] is the sub-list for method output_type + 51, // [51:75] is the sub-list for method input_type 51, // [51:51] is the sub-list for extension type_name 51, // [51:51] is the sub-list for extension extendee 0, // [0:51] is the sub-list for field type_name @@ -5564,7 +5624,7 @@ func file_odpf_optimus_runtime_service_proto_init() { } } file_odpf_optimus_runtime_service_proto_msgTypes[55].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReplayDryRunRequest); i { + switch v := v.(*ReplayRequest); i { case 0: return &v.state case 1: @@ -5599,7 +5659,19 @@ func file_odpf_optimus_runtime_service_proto_init() { return nil } } - file_odpf_optimus_runtime_service_proto_msgTypes[59].Exporter = func(v interface{}, i int) interface{} { + file_odpf_optimus_runtime_service_proto_msgTypes[58].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ReplayResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_odpf_optimus_runtime_service_proto_msgTypes[60].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ProjectSpecification_ProjectSecret); i { case 0: return &v.state @@ -5611,7 +5683,7 @@ func file_odpf_optimus_runtime_service_proto_init() { return nil } } - file_odpf_optimus_runtime_service_proto_msgTypes[63].Exporter = func(v interface{}, i int) interface{} { + file_odpf_optimus_runtime_service_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JobSpecification_Behavior); i { case 0: return &v.state @@ -5623,7 +5695,7 @@ func file_odpf_optimus_runtime_service_proto_init() { return nil } } - file_odpf_optimus_runtime_service_proto_msgTypes[64].Exporter = func(v interface{}, i int) interface{} { + file_odpf_optimus_runtime_service_proto_msgTypes[65].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*JobSpecification_Behavior_Retry); i { case 0: return &v.state @@ -5642,7 +5714,7 @@ func file_odpf_optimus_runtime_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_odpf_optimus_runtime_service_proto_rawDesc, NumEnums: 2, - NumMessages: 69, + NumMessages: 70, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/odpf/optimus/runtime_service.pb.gw.go b/api/proto/odpf/optimus/runtime_service.pb.gw.go index cdd10c8cc7..c4c8401a8d 100644 --- a/api/proto/odpf/optimus/runtime_service.pb.gw.go +++ b/api/proto/odpf/optimus/runtime_service.pb.gw.go @@ -1466,7 +1466,7 @@ var ( ) func request_RuntimeService_ReplayDryRun_0(ctx context.Context, marshaler runtime.Marshaler, client RuntimeServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ReplayDryRunRequest + var protoReq ReplayRequest var metadata runtime.ServerMetadata var ( @@ -1509,7 +1509,7 @@ func request_RuntimeService_ReplayDryRun_0(ctx context.Context, marshaler runtim } func local_request_RuntimeService_ReplayDryRun_0(ctx context.Context, marshaler runtime.Marshaler, server RuntimeServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ReplayDryRunRequest + var protoReq ReplayRequest var metadata runtime.ServerMetadata var ( @@ -1551,6 +1551,96 @@ func local_request_RuntimeService_ReplayDryRun_0(ctx context.Context, marshaler } +var ( + filter_RuntimeService_Replay_0 = &utilities.DoubleArray{Encoding: map[string]int{"project_name": 0, "job_name": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} +) + +func request_RuntimeService_Replay_0(ctx context.Context, marshaler runtime.Marshaler, client RuntimeServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ReplayRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["project_name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "project_name") + } + + protoReq.ProjectName, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "project_name", err) + } + + val, ok = pathParams["job_name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "job_name") + } + + protoReq.JobName, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "job_name", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RuntimeService_Replay_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Replay(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_RuntimeService_Replay_0(ctx context.Context, marshaler runtime.Marshaler, server RuntimeServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ReplayRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["project_name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "project_name") + } + + protoReq.ProjectName, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "project_name", err) + } + + val, ok = pathParams["job_name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "job_name") + } + + protoReq.JobName, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "job_name", err) + } + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RuntimeService_Replay_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Replay(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterRuntimeServiceHandlerServer registers the http handlers for service RuntimeService to "mux". // UnaryRPC :call RuntimeServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -2017,6 +2107,29 @@ func RegisterRuntimeServiceHandlerServer(ctx context.Context, mux *runtime.Serve }) + mux.Handle("POST", pattern_RuntimeService_Replay_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/odpf.optimus.RuntimeService/Replay") + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_RuntimeService_Replay_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_RuntimeService_Replay_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -2458,6 +2571,26 @@ func RegisterRuntimeServiceHandlerClient(ctx context.Context, mux *runtime.Serve }) + mux.Handle("POST", pattern_RuntimeService_Replay_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/odpf.optimus.RuntimeService/Replay") + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_RuntimeService_Replay_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_RuntimeService_Replay_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -2501,6 +2634,8 @@ var ( pattern_RuntimeService_UpdateResource_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 4, 2, 5, 1, 0, 4, 1, 5, 6, 2, 7}, []string{"api", "v1", "project", "project_name", "namespace", "datastore", "datastore_name", "resource"}, "")) pattern_RuntimeService_ReplayDryRun_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "project", "project_name", "job", "job_name", "replay-dry-run"}, "")) + + pattern_RuntimeService_Replay_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"api", "v1", "project", "project_name", "job", "job_name", "replay"}, "")) ) var ( @@ -2543,4 +2678,6 @@ var ( forward_RuntimeService_UpdateResource_0 = runtime.ForwardResponseMessage forward_RuntimeService_ReplayDryRun_0 = runtime.ForwardResponseMessage + + forward_RuntimeService_Replay_0 = runtime.ForwardResponseMessage ) diff --git a/api/proto/odpf/optimus/runtime_service_grpc.pb.go b/api/proto/odpf/optimus/runtime_service_grpc.pb.go index 917d8ff00d..0691ca0a9c 100644 --- a/api/proto/odpf/optimus/runtime_service_grpc.pb.go +++ b/api/proto/odpf/optimus/runtime_service_grpc.pb.go @@ -67,7 +67,8 @@ type RuntimeServiceClient interface { CreateResource(ctx context.Context, in *CreateResourceRequest, opts ...grpc.CallOption) (*CreateResourceResponse, error) ReadResource(ctx context.Context, in *ReadResourceRequest, opts ...grpc.CallOption) (*ReadResourceResponse, error) UpdateResource(ctx context.Context, in *UpdateResourceRequest, opts ...grpc.CallOption) (*UpdateResourceResponse, error) - ReplayDryRun(ctx context.Context, in *ReplayDryRunRequest, opts ...grpc.CallOption) (*ReplayDryRunResponse, error) + ReplayDryRun(ctx context.Context, in *ReplayRequest, opts ...grpc.CallOption) (*ReplayDryRunResponse, error) + Replay(ctx context.Context, in *ReplayRequest, opts ...grpc.CallOption) (*ReplayResponse, error) } type runtimeServiceClient struct { @@ -345,7 +346,7 @@ func (c *runtimeServiceClient) UpdateResource(ctx context.Context, in *UpdateRes return out, nil } -func (c *runtimeServiceClient) ReplayDryRun(ctx context.Context, in *ReplayDryRunRequest, opts ...grpc.CallOption) (*ReplayDryRunResponse, error) { +func (c *runtimeServiceClient) ReplayDryRun(ctx context.Context, in *ReplayRequest, opts ...grpc.CallOption) (*ReplayDryRunResponse, error) { out := new(ReplayDryRunResponse) err := c.cc.Invoke(ctx, "/odpf.optimus.RuntimeService/ReplayDryRun", in, out, opts...) if err != nil { @@ -354,6 +355,15 @@ func (c *runtimeServiceClient) ReplayDryRun(ctx context.Context, in *ReplayDryRu return out, nil } +func (c *runtimeServiceClient) Replay(ctx context.Context, in *ReplayRequest, opts ...grpc.CallOption) (*ReplayResponse, error) { + out := new(ReplayResponse) + err := c.cc.Invoke(ctx, "/odpf.optimus.RuntimeService/Replay", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RuntimeServiceServer is the server API for RuntimeService service. // All implementations must embed UnimplementedRuntimeServiceServer // for forward compatibility @@ -407,7 +417,8 @@ type RuntimeServiceServer interface { CreateResource(context.Context, *CreateResourceRequest) (*CreateResourceResponse, error) ReadResource(context.Context, *ReadResourceRequest) (*ReadResourceResponse, error) UpdateResource(context.Context, *UpdateResourceRequest) (*UpdateResourceResponse, error) - ReplayDryRun(context.Context, *ReplayDryRunRequest) (*ReplayDryRunResponse, error) + ReplayDryRun(context.Context, *ReplayRequest) (*ReplayDryRunResponse, error) + Replay(context.Context, *ReplayRequest) (*ReplayResponse, error) mustEmbedUnimplementedRuntimeServiceServer() } @@ -481,9 +492,12 @@ func (UnimplementedRuntimeServiceServer) ReadResource(context.Context, *ReadReso func (UnimplementedRuntimeServiceServer) UpdateResource(context.Context, *UpdateResourceRequest) (*UpdateResourceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateResource not implemented") } -func (UnimplementedRuntimeServiceServer) ReplayDryRun(context.Context, *ReplayDryRunRequest) (*ReplayDryRunResponse, error) { +func (UnimplementedRuntimeServiceServer) ReplayDryRun(context.Context, *ReplayRequest) (*ReplayDryRunResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ReplayDryRun not implemented") } +func (UnimplementedRuntimeServiceServer) Replay(context.Context, *ReplayRequest) (*ReplayResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Replay not implemented") +} func (UnimplementedRuntimeServiceServer) mustEmbedUnimplementedRuntimeServiceServer() {} // UnsafeRuntimeServiceServer may be embedded to opt out of forward compatibility for this service. @@ -903,7 +917,7 @@ func _RuntimeService_UpdateResource_Handler(srv interface{}, ctx context.Context } func _RuntimeService_ReplayDryRun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ReplayDryRunRequest) + in := new(ReplayRequest) if err := dec(in); err != nil { return nil, err } @@ -915,7 +929,25 @@ func _RuntimeService_ReplayDryRun_Handler(srv interface{}, ctx context.Context, FullMethod: "/odpf.optimus.RuntimeService/ReplayDryRun", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RuntimeServiceServer).ReplayDryRun(ctx, req.(*ReplayDryRunRequest)) + return srv.(RuntimeServiceServer).ReplayDryRun(ctx, req.(*ReplayRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeService_Replay_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReplayRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).Replay(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/odpf.optimus.RuntimeService/Replay", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).Replay(ctx, req.(*ReplayRequest)) } return interceptor(ctx, in, info, handler) } @@ -1007,6 +1039,10 @@ var RuntimeService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ReplayDryRun", Handler: _RuntimeService_ReplayDryRun_Handler, }, + { + MethodName: "Replay", + Handler: _RuntimeService_Replay_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/cmd/replay.go b/cmd/replay.go index 68add55571..a1f175dea6 100644 --- a/cmd/replay.go +++ b/cmd/replay.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/AlecAivazis/survey/v2" + "github.com/odpf/optimus/core/set" pb "github.com/odpf/optimus/api/proto/odpf/optimus" @@ -17,6 +19,10 @@ import ( "google.golang.org/grpc" ) +var ( + replayTimeout = time.Minute * 1 +) + type taskRunBlock struct { name string height int @@ -35,8 +41,8 @@ func taskRunBlockComperator(a, b interface{}) int { return strings.Compare(aAsserted.name, bAsserted.name) } -//formatRunsPerDAGInstance returns a hashmap with DAG -> Runs[] mapping -func formatRunsPerDAGInstance(instance *pb.ReplayExecutionTreeNode, taskReruns map[string]taskRunBlock, height int) { +//formatRunsPerJobInstance returns a hashmap with Job -> Runs[] mapping +func formatRunsPerJobInstance(instance *pb.ReplayExecutionTreeNode, taskReruns map[string]taskRunBlock, height int) { if _, ok := taskReruns[instance.JobName]; !ok { taskReruns[instance.JobName] = taskRunBlock{ name: instance.JobName, @@ -49,7 +55,7 @@ func formatRunsPerDAGInstance(instance *pb.ReplayExecutionTreeNode, taskReruns m taskReruns[instance.JobName].runs.Add(taskRun.AsTime()) } for _, child := range instance.Dependents { - formatRunsPerDAGInstance(child, taskReruns, height+1) + formatRunsPerJobInstance(child, taskReruns, height+1) } } @@ -108,6 +114,26 @@ ReplayDryRun date ranges are inclusive. //if only dry run, exit now return nil } + + proceedWithReplay := "Yes" + if err := survey.AskOne(&survey.Select{ + Message: "Proceed with replay?", + Options: []string{"Yes", "No"}, + Default: "Yes", + }, &proceedWithReplay); err != nil { + return err + } + + if proceedWithReplay == "No" { + l.Println("aborting...") + return nil + } + + replayId, err := runReplayRequest(l, replayProject, namespace, args[0], args[1], endDate, conf) + if err != nil { + return err + } + l.Printf("🚀 replay request created with id %s\n", replayId) return nil } return reCmd @@ -126,44 +152,42 @@ func printReplayExecutionTree(l logger, projectName, namespace, jobName, startDa } defer conn.Close() - dumpTimeoutCtx, dumpCancel := context.WithTimeout(context.Background(), renderTimeout) - defer dumpCancel() + replayRequestTimeout, replayRequestCancel := context.WithTimeout(context.Background(), replayTimeout) + defer replayRequestCancel() l.Println("please wait...") runtime := pb.NewRuntimeServiceClient(conn) - // fetch compiled JobSpec by calling the optimus API - replayDryRunRequest := &pb.ReplayDryRunRequest{ + replayRequest := &pb.ReplayRequest{ ProjectName: projectName, JobName: jobName, Namespace: namespace, StartDate: startDate, EndDate: endDate, } - replayDryRunResponse, err := runtime.ReplayDryRun(dumpTimeoutCtx, replayDryRunRequest) + replayDryRunResponse, err := runtime.ReplayDryRun(replayRequestTimeout, replayRequest) if err != nil { if errors.Is(err, context.DeadlineExceeded) { - l.Println("render process took too long, timing out") + l.Println("replay dry run took too long, timing out") } return errors.Wrapf(err, "request failed for job %s", jobName) } - printReplayDryRunResponse(l, replayDryRunRequest, replayDryRunResponse) + printReplayDryRunResponse(l, replayRequest, replayDryRunResponse) return nil } -func printReplayDryRunResponse(l logger, replayDryRunRequest *pb.ReplayDryRunRequest, replayDryRunResponse *pb.ReplayDryRunResponse) { - l.Printf("For %s project and %s namespace\n\n", coloredNotice(replayDryRunRequest.ProjectName), coloredNotice(replayDryRunRequest.Namespace)) +func printReplayDryRunResponse(l logger, replayRequest *pb.ReplayRequest, replayDryRunResponse *pb.ReplayDryRunResponse) { + l.Printf("For %s project and %s namespace\n\n", coloredNotice(replayRequest.ProjectName), coloredNotice(replayRequest.Namespace)) l.Println(coloredNotice("REPLAY RUNS")) table := tablewriter.NewWriter(l.Writer()) table.SetBorder(false) table.SetHeader([]string{ "Index", - "DAG", + "Job", "Run", }) - // generate basic details taskRerunsMap := make(map[string]taskRunBlock) - formatRunsPerDAGInstance(replayDryRunResponse.Response, taskRerunsMap, 0) + formatRunsPerJobInstance(replayDryRunResponse.Response, taskRerunsMap, 0) //sort run block taskRerunsSorted := set.NewTreeSetWith(taskRunBlockComperator) @@ -186,7 +210,7 @@ func printReplayDryRunResponse(l logger, replayDryRunRequest *pb.ReplayDryRunReq l.Println(fmt.Sprintf("%s", printExecutionTree(replayDryRunResponse.Response, treeprint.New()))) } -// PrintExecutionTree creates a ascii tree to visually inspect +// printExecutionTree creates a ascii tree to visually inspect // instance dependencies that will be recomputed after replay operation func printExecutionTree(instance *pb.ReplayExecutionTreeNode, tree treeprint.Tree) treeprint.Tree { subtree := tree.AddBranch(instance.JobName) @@ -202,3 +226,38 @@ func printExecutionTree(instance *pb.ReplayExecutionTreeNode, tree treeprint.Tre } return tree } + +func runReplayRequest(l logger, projectName, namespace, jobName, startDate, endDate string, conf config.Provider) (string, error) { + dialTimeoutCtx, dialCancel := context.WithTimeout(context.Background(), OptimusDialTimeout) + defer dialCancel() + + conn, err := createConnection(dialTimeoutCtx, conf.GetHost()) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + l.Println("can't reach optimus service") + } + return "", err + } + defer conn.Close() + + replayRequestTimeout, replayRequestCancel := context.WithTimeout(context.Background(), replayTimeout) + defer replayRequestCancel() + + l.Println("firing the replay request...") + runtime := pb.NewRuntimeServiceClient(conn) + replayRequest := &pb.ReplayRequest{ + ProjectName: projectName, + JobName: jobName, + Namespace: namespace, + StartDate: startDate, + EndDate: endDate, + } + replayResponse, err := runtime.Replay(replayRequestTimeout, replayRequest) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + l.Println("replay request took too long, timing out") + } + return "", errors.Wrapf(err, "request failed for job %s", jobName) + } + return replayResponse.Id, nil +} diff --git a/cmd/server/server.go b/cmd/server/server.go index 4da68d2eb2..0f4553097b 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -12,6 +12,8 @@ import ( "syscall" "time" + "github.com/odpf/optimus/utils" + "github.com/odpf/optimus/ext/scheduler/airflow" "github.com/odpf/optimus/config" @@ -69,6 +71,15 @@ func (fac *projectJobSpecRepoFactory) New(project models.ProjectSpec) store.Proj return postgres.NewProjectJobSpecRepository(fac.db, project, postgres.NewAdapter(models.TaskRegistry, models.HookRegistry)) } +type replaySpecRepoRepository struct { + db *gorm.DB + jobSpecRepoFac jobSpecRepoFactory +} + +func (fac *replaySpecRepoRepository) New(job models.JobSpec) store.ReplaySpecRepository { + return postgres.NewReplayRepository(fac.db, job) +} + // jobSpecRepoFactory stores raw specifications type jobSpecRepoFactory struct { db *gorm.DB @@ -226,7 +237,9 @@ func checkRequiredConfigs(conf config.Provider) error { if conf.GetServe().IngressHost == "" { return errors.Wrap(errRequiredMissing, "serve.ingress_host") } - + if conf.GetServe().ReplayNumWorkers < 1 { + return errors.New(fmt.Sprintf("%s should be greater than 0", config.KeyServeReplayNumWorkers)) + } if conf.GetServe().DB.DSN == "" { return errors.Wrap(errRequiredMissing, "serve.db.dsn") } @@ -380,6 +393,16 @@ func Initialize(conf config.Provider) error { projectResourceSpecRepoFac: projectResourceSpecRepoFac, } + replaySpecRepoFac := &replaySpecRepoRepository{ + db: dbConn, + jobSpecRepoFac: jobSpecRepoFac, + } + replayWorker := job.NewReplayWorker(replaySpecRepoFac, models.Scheduler) + replayManager := job.NewManager(replayWorker, replaySpecRepoFac, utils.NewUUIDProvider(), job.ReplayManagerConfig{ + NumWorkers: conf.GetServe().ReplayNumWorkers, + WorkerTimeout: conf.GetServe().ReplayWorkerTimeoutSecs, + }) + // runtime service instance over grpc pb.RegisterRuntimeServiceServer(grpcServer, v1handler.NewRuntimeServiceServer( config.Version, @@ -394,6 +417,7 @@ func Initialize(conf config.Provider) error { priorityResolver, metaSvcFactory, &projectJobSpecRepoFac, + replayManager, ), datastore.NewService(&resourceSpecRepoFac, models.DatastoreRegistry), projectRepoFac, @@ -466,6 +490,9 @@ func Initialize(conf config.Provider) error { // Block until we receive our signal. <-termChan mainLog.Info("termination request received") + if err = replayManager.Close(); err != nil { + return err + } // Create a deadline to wait for server ctxProxy, cancelProxy := context.WithTimeout(context.Background(), shutdownWait) diff --git a/config/config.go b/config/config.go index bf000e5d8a..b364a35e4e 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "strings" + "time" "github.com/knadh/koanf" ) @@ -32,6 +33,8 @@ var ( KeyServeMetadataKafkaBrokers = "serve.metadata.kafka_brokers" KeyServeMetadataKafkaJobTopic = "serve.metadata.kafka_job_topic" KeyServeMetadataKafkaBatchSize = "serve.metadata.kafka_batch_size" + KeyServeReplayNumWorkers = "serve.replay_num_workers" + KeyServeReplayWorkerTimeoutSecs = "serve.replay_worker_timeout_secs" KeySchedulerName = "scheduler.name" @@ -96,8 +99,10 @@ type ServerConfig struct { // random 32 character hash used for encrypting secrets AppKey string `yaml:"app_key"` - DB DBConfig `yaml:"db"` - Metadata MetadataConfig `yaml:"metadata"` + DB DBConfig `yaml:"db"` + Metadata MetadataConfig `yaml:"metadata"` + ReplayNumWorkers int `yaml:"replay_num_workers"` + ReplayWorkerTimeoutSecs time.Duration `yaml:"replay_worker_timeout_secs"` } type DBConfig struct { @@ -185,6 +190,8 @@ func (o Optimus) GetServe() ServerConfig { KafkaBrokers: o.eKs(KeyServeMetadataKafkaBrokers), KafkaBatchSize: o.eKi(KeyServeMetadataKafkaBatchSize), }, + ReplayNumWorkers: o.k.Int(KeyServeReplayNumWorkers), + ReplayWorkerTimeoutSecs: time.Second * time.Duration(o.k.Int(KeyServeReplayWorkerTimeoutSecs)), } } diff --git a/config/loader.go b/config/loader.go index 1c59cf1e3d..349515c8ce 100644 --- a/config/loader.go +++ b/config/loader.go @@ -59,6 +59,8 @@ func InitOptimus() (*Optimus, error) { KeyServeMetadataKafkaBatchSize: 50, KeyServeMetadataWriterBatchSize: 50, KeySchedulerName: "airflow2", + KeyServeReplayNumWorkers: 1, + KeyServeReplayWorkerTimeoutSecs: 120, }, "."), nil); err != nil { return nil, errors.Wrap(err, "k.Load: error loading config defaults") } diff --git a/core/bus/bus.go b/core/bus/bus.go deleted file mode 100644 index 6519950968..0000000000 --- a/core/bus/bus.go +++ /dev/null @@ -1,75 +0,0 @@ -package bus - -// allows independent components of an application to -// observe events produced by decoupled producers -// -// producer of "someevent" -// bus.Post("someevent", "data") -// -// observer of "someevent" -// myChan := make(chan string) -// bus.Listen("someevent", myChan) -// for { -// data := <-myChan -// fmt.Printf("someevent: %s", data) -// } -// -// make sure these events are unique - -import ( - "errors" - "sync" -) - -var ( - ErrNotFound = errors.New("not found") -) - -var ( - // mapping of event to listening channels - eventBus = make(map[string][]chan<- interface{}) - rwMutex sync.RWMutex -) - -// Listen observing the specified event via provided channel -func Listen(event string, out chan interface{}) { - rwMutex.Lock() - defer rwMutex.Unlock() - eventBus[event] = append(eventBus[event], out) -} - -// Stop observing the specified event on the channel -func Stop(event string, out chan interface{}) error { - rwMutex.Lock() - defer rwMutex.Unlock() - - newEventBus := make([]chan<- interface{}, 0) - outChans, ok := eventBus[event] - if !ok { - return ErrNotFound - } - for _, ch := range outChans { - if ch != out { - newEventBus = append(newEventBus, ch) - } - } - eventBus[event] = newEventBus - - return nil -} - -// Post a notification to the specified event -func Post(event string, data interface{}) error { - rwMutex.RLock() - defer rwMutex.RUnlock() - - if listeners, ok := eventBus[event]; ok { - //push this to all listeners - for _, out := range listeners { - out <- data - } - } else { - return ErrNotFound - } - return nil -} diff --git a/core/tree/multi_root_tree_test.go b/core/tree/multi_root_tree_test.go index f1d725b2e1..f56a1f7e94 100644 --- a/core/tree/multi_root_tree_test.go +++ b/core/tree/multi_root_tree_test.go @@ -1,7 +1,6 @@ package tree_test import ( - "strings" "testing" "github.com/odpf/optimus/core/tree" @@ -11,13 +10,13 @@ import ( func TestMultiRootDagTree(t *testing.T) { t.Run("GetNameAndDependents", func(t *testing.T) { - multiRootTree := tree.NewMultiRootTree() treeNode1 := tree.NewTreeNode(models.JobSpec{ Name: "job1", }) treeNode2 := tree.NewTreeNode(models.JobSpec{ Name: "job2", }) + multiRootTree := tree.NewMultiRootTree() treeNode1.AddDependent(treeNode2) treeNode2.AddDependent(treeNode1) multiRootTree.AddNodeIfNotExist(treeNode1) @@ -25,6 +24,49 @@ func TestMultiRootDagTree(t *testing.T) { err := multiRootTree.IsCyclic() assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), tree.ErrCyclicDependencyEncountered.Error())) + assert.Contains(t, err.Error(), tree.ErrCyclicDependencyEncountered.Error()) + }) + t.Run("MarkRoot", func(t *testing.T) { + treeNode1 := tree.NewTreeNode(models.JobSpec{ + Name: "job1", + }) + multiRootTree := tree.NewMultiRootTree() + multiRootTree.AddNode(treeNode1) + multiRootTree.MarkRoot(treeNode1) + rootNodes := multiRootTree.GetRootNodes() + assert.Equal(t, 1, len(rootNodes)) + assert.Equal(t, "job1", rootNodes[0].Data.GetName()) + }) + t.Run("IsCyclic", func(t *testing.T) { + t.Run("should throw an error if cyclic", func(t *testing.T) { + treeNode1 := tree.NewTreeNode(models.JobSpec{ + Name: "job1", + }) + treeNode2 := tree.NewTreeNode(models.JobSpec{ + Name: "job2", + }) + multiRootTree := tree.NewMultiRootTree() + multiRootTree.AddNode(treeNode1) + multiRootTree.AddNode(treeNode2) + treeNode1.AddDependent(treeNode2) + treeNode2.AddDependent(treeNode1) + err := multiRootTree.IsCyclic() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "cycle dependency") + }) + t.Run("should not return error if not cyclic", func(t *testing.T) { + treeNode1 := tree.NewTreeNode(models.JobSpec{ + Name: "job1", + }) + treeNode2 := tree.NewTreeNode(models.JobSpec{ + Name: "job2", + }) + multiRootTree := tree.NewMultiRootTree() + multiRootTree.AddNode(treeNode1) + multiRootTree.AddNode(treeNode2) + treeNode1.AddDependent(treeNode2) + err := multiRootTree.IsCyclic() + assert.Nil(t, err) + }) }) } diff --git a/core/tree/tree_node.go b/core/tree/tree_node.go index 5685f64b15..1d18141e33 100644 --- a/core/tree/tree_node.go +++ b/core/tree/tree_node.go @@ -15,6 +15,20 @@ type TreeNode struct { Runs set.Set } +// GetAllNodes returns level order traversal of tree starting from current node +func (t *TreeNode) GetAllNodes() []*TreeNode { + allNodes := make([]*TreeNode, 0) + nodesQueue := make([]*TreeNode, 0) + nodesQueue = append(nodesQueue, t) + for len(nodesQueue) != 0 { + topNode := nodesQueue[0] + nodesQueue = nodesQueue[1:] + allNodes = append(allNodes, topNode) + nodesQueue = append(nodesQueue, topNode.Dependents...) + } + return allNodes +} + func (t *TreeNode) GetName() string { return t.Data.GetName() } diff --git a/core/tree/tree_node_test.go b/core/tree/tree_node_test.go index 6b5547c081..c411dc9100 100644 --- a/core/tree/tree_node_test.go +++ b/core/tree/tree_node_test.go @@ -24,4 +24,30 @@ func TestDagNode(t *testing.T) { dagNode.AddDependent(dependentDagNode) assert.Equal(t, jobName, dagNode.GetName()) }) + t.Run("GetAllNodes", func(t *testing.T) { + treeNode := tree.TreeNode{ + Data: models.JobSpec{ + Name: "job-level-0", + }, + Dependents: []*tree.TreeNode{ + { + Data: models.JobSpec{ + Name: "job-level-1", + }, + Dependents: []*tree.TreeNode{ + { + Data: models.JobSpec{ + Name: "job-level-2", + }, + }, + }, + }, + }, + } + allNodes := treeNode.GetAllNodes() + assert.Equal(t, 3, len(allNodes)) + assert.Equal(t, "job-level-0", allNodes[0].Data.GetName()) + assert.Equal(t, "job-level-1", allNodes[1].Data.GetName()) + assert.Equal(t, "job-level-2", allNodes[2].Data.GetName()) + }) } diff --git a/ext/scheduler/airflow/airflow.go b/ext/scheduler/airflow/airflow.go index d9f20007d8..7b8f552147 100644 --- a/ext/scheduler/airflow/airflow.go +++ b/ext/scheduler/airflow/airflow.go @@ -29,6 +29,7 @@ var resBaseDAG []byte const ( baseLibFileName = "__lib.py" dagStatusURL = "api/experimental/dags/%s/dag_runs" + dagRunClearURL = "clear&dag_id=%s&start_date=%s&end_date=%s" ) type HTTPClient interface { @@ -170,3 +171,54 @@ func (a *scheduler) GetJobStatus(ctx context.Context, projSpec models.ProjectSpe return jobStatus, nil } + +func (a *scheduler) Clear(ctx context.Context, projSpec models.ProjectSpec, jobName string, startDate, endDate time.Time) error { + schdHost, ok := projSpec.Config[models.ProjectSchedulerHost] + if !ok { + return errors.Errorf("scheduler host not set for %s", projSpec.Name) + } + + schdHost = strings.Trim(schdHost, "/") + airflowDateFormat := "2006-01-02T15:04:05" + utcTimezone, _ := time.LoadLocation("UTC") + clearDagRunURL := fmt.Sprintf( + fmt.Sprintf("%s/%s", schdHost, dagRunClearURL), + jobName, + startDate.In(utcTimezone).Format(airflowDateFormat), + endDate.In(utcTimezone).Format(airflowDateFormat)) + request, err := http.NewRequest(http.MethodGet, clearDagRunURL, nil) + if err != nil { + return errors.Wrapf(err, "failed to build http request for %s", clearDagRunURL) + } + + resp, err := a.httpClient.Do(request) + if err != nil { + return errors.Wrapf(err, "failed to clear airflow dag runs from %s", clearDagRunURL) + } + if resp.StatusCode != http.StatusOK { + return errors.Errorf("failed to clear airflow dag runs from %s", clearDagRunURL) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "failed to read airflow response") + } + + //{ + // "http_response_code": 200, + // "status": "status" + //} + responseJSON := map[string]interface{}{} + err = json.Unmarshal(body, &responseJSON) + if err != nil { + return errors.Wrapf(err, "json error: %s", string(body)) + } + + responseFields := []string{"http_response_code", "status"} + for _, field := range responseFields { + if _, ok := responseJSON[field]; !ok { + return errors.Errorf("failed to find required response fields %s in %s", field, responseJSON) + } + } + return nil +} diff --git a/ext/scheduler/airflow/airflow_test.go b/ext/scheduler/airflow/airflow_test.go index 087f1a9b6f..fa64d82420 100644 --- a/ext/scheduler/airflow/airflow_test.go +++ b/ext/scheduler/airflow/airflow_test.go @@ -7,6 +7,9 @@ import ( "io/ioutil" "net/http" "testing" + "time" + + "github.com/odpf/optimus/job" "github.com/odpf/optimus/store" "github.com/stretchr/testify/mock" @@ -164,4 +167,61 @@ func TestAirflow(t *testing.T) { assert.Len(t, status, 0) }) }) + t.Run("Clear", func(t *testing.T) { + host := "http://airflow.example.io" + startDate := "2021-05-20" + startDateTime, _ := time.Parse(job.ReplayDateFormat, startDate) + endDate := "2021-05-25" + endDateTime, _ := time.Parse(job.ReplayDateFormat, endDate) + + t.Run("should return job status with valid args", func(t *testing.T) { + respString := ` +{ + "http_response_code": 200, + "status": "success" +}` + // create a new reader with JSON + r := ioutil.NopCloser(bytes.NewReader([]byte(respString))) + client := &MockHttpClient{ + DoFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: r, + }, nil + }, + } + + air := airflow.NewScheduler(nil, client) + err := air.Clear(ctx, models.ProjectSpec{ + Name: "test-proj", + Config: map[string]string{ + models.ProjectSchedulerHost: host, + }, + }, "sample_select", startDateTime, endDateTime) + + assert.Nil(t, err) + }) + t.Run("should fail if host fails to return OK", func(t *testing.T) { + respString := `INTERNAL ERROR` + r := ioutil.NopCloser(bytes.NewReader([]byte(respString))) + client := &MockHttpClient{ + DoFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: r, + }, nil + }, + } + + air := airflow.NewScheduler(nil, client) + err := air.Clear(ctx, models.ProjectSpec{ + Name: "test-proj", + Config: map[string]string{ + models.ProjectSchedulerHost: host, + }, + }, "sample_select", startDateTime, endDateTime) + + assert.NotNil(t, err) + }) + }) } diff --git a/ext/scheduler/airflow2/airflow.go b/ext/scheduler/airflow2/airflow.go index 778a3f9745..c2301b562e 100644 --- a/ext/scheduler/airflow2/airflow.go +++ b/ext/scheduler/airflow2/airflow.go @@ -29,6 +29,7 @@ var resBaseDAG []byte const ( baseLibFileName = "__lib.py" dagStatusUrl = "api/v1/dags/%s/dagRuns" + dagRunClearURL = "api/v1/dags/%s/clearTaskInstances" ) type HttpClient interface { @@ -177,3 +178,36 @@ func (a *scheduler) GetJobStatus(ctx context.Context, projSpec models.ProjectSpe return jobStatus, nil } + +func (a *scheduler) Clear(ctx context.Context, projSpec models.ProjectSpec, jobName string, startDate, endDate time.Time) error { + schdHost, ok := projSpec.Config[models.ProjectSchedulerHost] + if !ok { + return errors.Errorf("scheduler host not set for %s", projSpec.Name) + } + + schdHost = strings.Trim(schdHost, "/") + airflowDateFormat := "2006-01-02T15:04:05+00:00" + var jsonStr = []byte(fmt.Sprintf(`{"start_date":"%s", "end_date": "%s", "dry_run": false}`, + startDate.UTC().Format(airflowDateFormat), + endDate.UTC().Format(airflowDateFormat))) + postURL := fmt.Sprintf( + fmt.Sprintf("%s/%s", schdHost, dagRunClearURL), + jobName) + + request, err := http.NewRequest(http.MethodPost, postURL, bytes.NewBuffer(jsonStr)) + if err != nil { + return errors.Wrapf(err, "failed to build http request for %s", postURL) + } + request.Header.Set("Content-Type", "application/json") + + resp, err := a.httpClient.Do(request) + if err != nil { + return errors.Wrapf(err, "failed to clear airflow dag runs from %s", postURL) + } + if resp.StatusCode != http.StatusOK { + return errors.Errorf("failed to clear airflow dag runs from %s", postURL) + } + defer resp.Body.Close() + + return nil +} diff --git a/ext/scheduler/airflow2/airflow_test.go b/ext/scheduler/airflow2/airflow_test.go index 1adcfa1834..03d220c16e 100644 --- a/ext/scheduler/airflow2/airflow_test.go +++ b/ext/scheduler/airflow2/airflow_test.go @@ -7,6 +7,9 @@ import ( "io/ioutil" "net/http" "testing" + "time" + + "github.com/odpf/optimus/job" "github.com/odpf/optimus/store" "github.com/stretchr/testify/mock" @@ -167,4 +170,56 @@ func TestAirflow2(t *testing.T) { assert.Len(t, status, 0) }) }) + t.Run("Clear", func(t *testing.T) { + host := "http://airflow.example.io" + startDate := "2021-05-20" + startDateTime, _ := time.Parse(job.ReplayDateFormat, startDate) + endDate := "2021-05-25" + endDateTime, _ := time.Parse(job.ReplayDateFormat, endDate) + + t.Run("should clear dagrun state successfully", func(t *testing.T) { + // create a new reader with JSON + r := ioutil.NopCloser(bytes.NewReader([]byte(""))) + client := &MockHttpClient{ + DoFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: r, + }, nil + }, + } + + air := airflow2.NewScheduler(nil, client) + err := air.Clear(ctx, models.ProjectSpec{ + Name: "test-proj", + Config: map[string]string{ + models.ProjectSchedulerHost: host, + }, + }, "sample_select", startDateTime, endDateTime) + + assert.Nil(t, err) + }) + t.Run("should fail if host fails to return OK", func(t *testing.T) { + respString := `INTERNAL ERROR` + r := ioutil.NopCloser(bytes.NewReader([]byte(respString))) + client := &MockHttpClient{ + DoFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + Body: r, + }, nil + }, + } + + air := airflow2.NewScheduler(nil, client) + err := air.Clear(ctx, models.ProjectSpec{ + Name: "test-proj", + Config: map[string]string{ + models.ProjectSchedulerHost: host, + }, + }, "sample_select", startDateTime, endDateTime) + + assert.NotNil(t, err) + }) + }) } diff --git a/job/priority_resolver.go b/job/priority_resolver.go index 6803616f16..30ac99b1c7 100644 --- a/job/priority_resolver.go +++ b/job/priority_resolver.go @@ -100,19 +100,19 @@ func (a *priorityResolver) assignWeight(rootNodes []*tree.TreeNode, weight int, // based on the dependencies of each DAG. func (a *priorityResolver) buildMultiRootDependencyTree(jobSpecs []models.JobSpec) (*tree.MultiRootTree, error) { // creates map[jobName]jobSpec for faster retrieval - dagSpecMap := make(map[string]models.JobSpec) + jobSpecMap := make(map[string]models.JobSpec) for _, dagSpec := range jobSpecs { - dagSpecMap[dagSpec.Name] = dagSpec + jobSpecMap[dagSpec.Name] = dagSpec } // build a multi root tree and assign dependencies // ignore any other dependency apart from intra-tenant tree := tree.NewMultiRootTree() - for _, childSpec := range dagSpecMap { + for _, childSpec := range jobSpecMap { childNode := a.findOrCreateDAGNode(tree, childSpec) for _, depDAG := range childSpec.Dependencies { var isExternal = false - parentSpec, ok := dagSpecMap[depDAG.Job.Name] + parentSpec, ok := jobSpecMap[depDAG.Job.Name] if !ok { if depDAG.Type == models.JobSpecDependencyTypeIntra { return nil, errors.Wrap(ErrJobSpecNotFound, depDAG.Job.Name) diff --git a/job/priority_resolver_test.go b/job/priority_resolver_test.go index 8222e15248..799c2ac48d 100644 --- a/job/priority_resolver_test.go +++ b/job/priority_resolver_test.go @@ -1,7 +1,6 @@ package job_test import ( - "strings" "testing" "github.com/odpf/optimus/core/tree" @@ -336,7 +335,7 @@ func TestPriorityWeightResolver(t *testing.T) { assginer := job.NewPriorityResolver() _, err := assginer.Resolve(dagSpec) assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), tree.ErrCyclicDependencyEncountered.Error())) + assert.Contains(t, err.Error(), tree.ErrCyclicDependencyEncountered.Error()) }) t.Run("Resolve should assign correct weights (maxWeight) with no dependencies", func(t *testing.T) { @@ -524,7 +523,7 @@ func TestMultiRootDAGTree(t *testing.T) { err := dagTree.IsCyclic() assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), tree.ErrCyclicDependencyEncountered.Error())) + assert.Contains(t, err.Error(), tree.ErrCyclicDependencyEncountered.Error()) }) t.Run("should create tree with multi level dependencies", func(t *testing.T) { @@ -653,6 +652,6 @@ func TestMultiRootDAGTree(t *testing.T) { err := dagTree.IsCyclic() assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), tree.ErrCyclicDependencyEncountered.Error())) + assert.Contains(t, err.Error(), tree.ErrCyclicDependencyEncountered.Error()) }) } diff --git a/job/replay.go b/job/replay.go index 42a219b853..b1ecee303d 100644 --- a/job/replay.go +++ b/job/replay.go @@ -15,18 +15,26 @@ const ( ReplayDateFormat = "2006-01-02" ) -func (srv *Service) ReplayDryRun(namespace models.NamespaceSpec, replayJobSpec models.JobSpec, start, end time.Time) (*tree.TreeNode, error) { - projectJobSpecRepo := srv.projectJobSpecRepoFactory.New(namespace.ProjectSpec) - jobSpecs, err := srv.getDependencyResolvedSpecs(namespace.ProjectSpec, projectJobSpecRepo, nil) +func (srv *Service) populateRequestWithJobSpecs(replayRequest *models.ReplayWorkerRequest) error { + projectJobSpecRepo := srv.projectJobSpecRepoFactory.New(replayRequest.Project) + jobSpecs, err := srv.getDependencyResolvedSpecs(replayRequest.Project, projectJobSpecRepo, nil) if err != nil { - return nil, err + return err } - dagSpecMap := make(map[string]models.JobSpec) + jobSpecMap := make(map[string]models.JobSpec) for _, currSpec := range jobSpecs { - dagSpecMap[currSpec.Name] = currSpec + jobSpecMap[currSpec.Name] = currSpec + } + replayRequest.JobSpecMap = jobSpecMap + return nil +} + +func (srv *Service) ReplayDryRun(replayRequest *models.ReplayWorkerRequest) (*tree.TreeNode, error) { + if err := srv.populateRequestWithJobSpecs(replayRequest); err != nil { + return nil, err } - rootInstance, err := prepareTree(dagSpecMap, replayJobSpec.Name, start, end) + rootInstance, err := prepareTree(replayRequest) if err != nil { return nil, err } @@ -34,17 +42,29 @@ func (srv *Service) ReplayDryRun(namespace models.NamespaceSpec, replayJobSpec m return rootInstance, nil } +func (srv *Service) Replay(replayRequest *models.ReplayWorkerRequest) (string, error) { + if err := srv.populateRequestWithJobSpecs(replayRequest); err != nil { + return "", err + } + + replayUUID, err := srv.replayManager.Replay(replayRequest) + if err != nil { + return "", err + } + return replayUUID, nil +} + // prepareTree creates a execution tree for replay operation -func prepareTree(dagSpecMap map[string]models.JobSpec, replayJobName string, start, end time.Time) (*tree.TreeNode, error) { - replayJobSpec, found := dagSpecMap[replayJobName] +func prepareTree(replayRequest *models.ReplayWorkerRequest) (*tree.TreeNode, error) { + replayJobSpec, found := replayRequest.JobSpecMap[replayRequest.Job.Name] if !found { - return nil, fmt.Errorf("couldn't find any job with name %s", replayJobName) + return nil, fmt.Errorf("couldn't find any job with name %s", replayRequest.Job.Name) } // compute runs that require replay dagTree := tree.NewMultiRootTree() parentNode := tree.NewTreeNode(replayJobSpec) - if runs, err := getRunsBetweenDates(start, end, replayJobSpec.Schedule.Interval); err == nil { + if runs, err := getRunsBetweenDates(replayRequest.Start, replayRequest.End, replayJobSpec.Schedule.Interval); err == nil { for _, run := range runs { parentNode.Runs.Add(run) } @@ -53,7 +73,7 @@ func prepareTree(dagSpecMap map[string]models.JobSpec, replayJobName string, sta } dagTree.AddNode(parentNode) - rootInstance, err := populateDownstreamDAGs(dagTree, replayJobSpec, dagSpecMap) + rootInstance, err := populateDownstreamDAGs(dagTree, replayJobSpec, replayRequest.JobSpecMap) if err != nil { return nil, err } @@ -75,12 +95,12 @@ func findOrCreateDAGNode(dagTree *tree.MultiRootTree, dagSpec models.JobSpec) *t return node } -func populateDownstreamDAGs(dagTree *tree.MultiRootTree, jobSpec models.JobSpec, dagSpecMap map[string]models.JobSpec) (*tree.TreeNode, error) { - for _, childSpec := range dagSpecMap { +func populateDownstreamDAGs(dagTree *tree.MultiRootTree, jobSpec models.JobSpec, jobSpecMap map[string]models.JobSpec) (*tree.TreeNode, error) { + for _, childSpec := range jobSpecMap { childNode := findOrCreateDAGNode(dagTree, childSpec) for _, depDAG := range childSpec.Dependencies { var isExternal = false - parentSpec, ok := dagSpecMap[depDAG.Job.Name] + parentSpec, ok := jobSpecMap[depDAG.Job.Name] if !ok { if depDAG.Type == models.JobSpecDependencyTypeIntra { return nil, errors.Wrap(ErrJobSpecNotFound, depDAG.Job.Name) diff --git a/job/replay_manager.go b/job/replay_manager.go new file mode 100644 index 0000000000..f03bbb93ec --- /dev/null +++ b/job/replay_manager.go @@ -0,0 +1,142 @@ +package job + +import ( + "context" + "sync" + "time" + + "github.com/odpf/optimus/utils" + + "github.com/google/uuid" + "github.com/odpf/optimus/core/logger" + "github.com/odpf/optimus/models" + "github.com/pkg/errors" +) + +var ( + // ErrRequestQueueFull signifies that the deployment manager's + // request queue is full + ErrRequestQueueFull = errors.New("request queue is full") +) + +type ReplayManagerConfig struct { + NumWorkers int + WorkerTimeout time.Duration +} + +type ReplayManager interface { + Init() + Replay(*models.ReplayWorkerRequest) (string, error) +} + +// Manager for replaying operation(s). +// Offers an asynchronous interface to pipeline, with a fixed size request queue +// Each replay request is handled by a replay worker and the number of parallel replay workers +// can be provided through configuration. +type Manager struct { + // wait group to synchronise on workers + wg sync.WaitGroup + mu sync.Mutex + + uuidProvider utils.UUIDProvider + config ReplayManagerConfig + + // request queue, used by workers + requestQ chan *models.ReplayWorkerRequest + // request map, used for verifying if a request is + // in queue without actually consuming it + requestMap map[uuid.UUID]bool + + //request worker + replayWorker ReplayWorker + replaySpecRepoFac ReplaySpecRepoFactory +} + +// Replay a request asynchronously, returns a replay id that can +// can be used to query its status +func (m *Manager) Replay(reqInput *models.ReplayWorkerRequest) (string, error) { + uuidOb, err := m.uuidProvider.NewUUID() + if err != nil { + return "", err + } + reqInput.ID = uuidOb + + // save replay request and mark status as accepted + replay := models.ReplaySpec{ + ID: uuidOb, + Job: reqInput.Job, + StartDate: reqInput.Start, + EndDate: reqInput.End, + Status: models.ReplayStatusAccepted, + } + replaySpecRepo := m.replaySpecRepoFac.New(reqInput.Job) + if err = replaySpecRepo.Insert(&replay); err != nil { + return "", err + } + + // try sending the job request down the request queue + // if full return error indicating that we don't have capacity + // to process this request at the moment + select { + case m.requestQ <- reqInput: + m.mu.Lock() + //request pushed to worker + m.requestMap[reqInput.ID] = true + m.mu.Unlock() + + return reqInput.ID.String(), nil + default: + return "", ErrRequestQueueFull + } +} + +// start a worker goroutine that runs the deployment pipeline in background +func (m *Manager) spawnServiceWorker() { + defer m.wg.Done() + + for reqInput := range m.requestQ { + logger.I("worker picked up the request for ", reqInput.Job.Name) + ctx, cancelCtx := context.WithTimeout(context.Background(), m.config.WorkerTimeout) + if err := m.replayWorker.Process(ctx, reqInput); err != nil { + //do something about this error + logger.E(errors.Wrap(err, "worker failed to process")) + cancelCtx() + } + cancelCtx() + } +} + +//Close stops consuming any new request +func (m *Manager) Close() error { + if m.requestQ != nil { + //stop accepting any more requests + close(m.requestQ) + } + + //wait for request worker to finish + m.wg.Wait() + + return nil +} + +func (m *Manager) Init() { + logger.I("starting replay workers") + for i := 0; i < m.config.NumWorkers; i++ { + m.wg.Add(1) + go m.spawnServiceWorker() + } +} + +// NewManager constructs a new instance of Manager +func NewManager(worker ReplayWorker, replaySpecRepoFac ReplaySpecRepoFactory, uuidProvider utils.UUIDProvider, config ReplayManagerConfig) *Manager { + mgr := &Manager{ + replayWorker: worker, + requestMap: make(map[uuid.UUID]bool), + config: config, + requestQ: make(chan *models.ReplayWorkerRequest, 0), + replaySpecRepoFac: replaySpecRepoFac, + uuidProvider: uuidProvider, + } + mgr.Init() + return mgr +} diff --git a/job/replay_manager_test.go b/job/replay_manager_test.go new file mode 100644 index 0000000000..1eadd412d2 --- /dev/null +++ b/job/replay_manager_test.go @@ -0,0 +1,91 @@ +package job_test + +import ( + "io/ioutil" + "testing" + "time" + + "github.com/odpf/optimus/core/logger" + + "github.com/google/uuid" + "github.com/odpf/optimus/job" + "github.com/odpf/optimus/mock" + "github.com/odpf/optimus/models" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestReplayManager(t *testing.T) { + logger.InitWithWriter(logger.DEBUG, ioutil.Discard) + replayManagerConfig := job.ReplayManagerConfig{ + NumWorkers: 5, + WorkerTimeout: 1000, + } + t.Run("Close", func(t *testing.T) { + manager := job.NewManager(nil, nil, nil, replayManagerConfig) + err := manager.Close() + assert.Nil(t, err) + }) + t.Run("Replay", func(t *testing.T) { + dagStartTime, _ := time.Parse(job.ReplayDateFormat, "2020-04-05") + startDate, _ := time.Parse(job.ReplayDateFormat, "2020-08-22") + endDate, _ := time.Parse(job.ReplayDateFormat, "2020-08-26") + jobSpec := models.JobSpec{ + Name: "job-name", + Schedule: models.JobSpecSchedule{ + StartDate: dagStartTime, + Interval: "0 2 * * *", + }, + } + replayRequest := &models.ReplayWorkerRequest{ + Job: jobSpec, + Start: startDate, + End: endDate, + Project: models.ProjectSpec{ + Name: "project-name", + }, + JobSpecMap: map[string]models.JobSpec{ + "job-name": jobSpec, + }, + } + t.Run("should throw error if uuid provider returns failure", func(t *testing.T) { + uuidProvider := new(mock.UUIDProvider) + defer uuidProvider.AssertExpectations(t) + objUUID := uuid.Must(uuid.NewRandom()) + errMessage := "error while generating uuid" + uuidProvider.On("NewUUID").Return(objUUID, errors.New(errMessage)) + + replayManager := job.NewManager(nil, nil, uuidProvider, replayManagerConfig) + _, err := replayManager.Replay(replayRequest) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), errMessage) + }) + t.Run("should throw an error if replay repo throws error", func(t *testing.T) { + replayRepository := new(mock.ReplayRepository) + defer replayRepository.AssertExpectations(t) + replaySpecRepoFac := new(mock.ReplaySpecRepoFactory) + defer replaySpecRepoFac.AssertExpectations(t) + replaySpecRepoFac.On("New", replayRequest.Job).Return(replayRepository) + + uuidProvider := new(mock.UUIDProvider) + defer uuidProvider.AssertExpectations(t) + objUUID := uuid.Must(uuid.NewRandom()) + uuidProvider.On("NewUUID").Return(objUUID, nil) + + errMessage := "error with replay repo" + toInsertReplaySpec := &models.ReplaySpec{ + ID: objUUID, + Job: jobSpec, + StartDate: startDate, + EndDate: endDate, + Status: models.ReplayStatusAccepted, + } + replayRepository.On("Insert", toInsertReplaySpec).Return(errors.New(errMessage)) + + replayManager := job.NewManager(nil, replaySpecRepoFac, uuidProvider, replayManagerConfig) + _, err := replayManager.Replay(replayRequest) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), errMessage) + }) + }) +} diff --git a/job/replay_test.go b/job/replay_test.go index bd4bb06dcb..0eb031486c 100644 --- a/job/replay_test.go +++ b/job/replay_test.go @@ -1,17 +1,18 @@ package job_test import ( - "strings" "testing" "time" + "github.com/google/uuid" + + "github.com/odpf/optimus/job" + "github.com/odpf/optimus/core/tree" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" - "github.com/google/uuid" - "github.com/odpf/optimus/job" "github.com/odpf/optimus/mock" "github.com/odpf/optimus/models" "github.com/stretchr/testify/assert" @@ -91,187 +92,319 @@ func TestReplay(t *testing.T) { Name: "proj", } - namespaceSpec := models.NamespaceSpec{ - ID: uuid.Must(uuid.NewRandom()), - Name: "dev-team-1", - ProjectSpec: projSpec, - } - - t.Run("should fail if unable to fetch jobSpecs from project jobSpecRepo", func(t *testing.T) { - projectJobSpecRepo := new(mock.ProjectJobSpecRepository) - projectJobSpecRepo.On("GetAll").Return(nil, errors.New("error while getting all dags")) - defer projectJobSpecRepo.AssertExpectations(t) - - projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) - projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) - defer projJobSpecRepoFac.AssertExpectations(t) - - replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") - replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") - - jobSvc := job.NewService(nil, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac) - _, err := jobSvc.ReplayDryRun(namespaceSpec, specs[spec1], replayStart, replayEnd) - - assert.NotNil(t, err) + t.Run("ReplayDryRun", func(t *testing.T) { + t.Run("should fail if unable to fetch jobSpecs from project jobSpecRepo", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(nil, errors.New("error while getting all dags")) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + + jobSvc := job.NewService(nil, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac, nil) + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec1], + Start: replayStart, + End: replayEnd, + Project: projSpec, + } + _, err := jobSvc.ReplayDryRun(replayRequest) + + assert.NotNil(t, err) + }) + + t.Run("should fail if unable to resolve jobs using dependency resolver", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + // resolve dependencies + depenResolver := new(mock.DependencyResolver) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(models.JobSpec{}, errors.New("error while fetching dag1")) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(models.JobSpec{}, errors.New("error while fetching dag3")) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(models.JobSpec{}, errors.New("error while fetching dag4")) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) + defer depenResolver.AssertExpectations(t) + + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + + jobSvc := job.NewService(nil, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, nil) + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec1], + Start: replayStart, + End: replayEnd, + Project: projSpec, + } + _, err := jobSvc.ReplayDryRun(replayRequest) + + assert.NotNil(t, err) + merr := err.(*multierror.Error) + assert.Equal(t, 3, merr.Len()) + }) + + t.Run("should fail if tree is cyclic", func(t *testing.T) { + cyclicDagSpec := make([]models.JobSpec, 0) + cyclicDag1 := models.JobSpec{Name: "dag1-deps-on-dag2", Schedule: twoAMSchedule, Task: oneDayTaskWindow} + cyclicDag2 := models.JobSpec{Name: "dag2-deps-on-dag1", Schedule: twoAMSchedule, Task: oneDayTaskWindow} + cyclicDag1Deps := make(map[string]models.JobSpecDependency) + cyclicDag1Deps[cyclicDag1.Name] = models.JobSpecDependency{Job: &cyclicDag2} + cyclicDag2Deps := make(map[string]models.JobSpecDependency) + cyclicDag2Deps[cyclicDag2.Name] = models.JobSpecDependency{Job: &cyclicDag1} + cyclicDag1.Dependencies = cyclicDag1Deps + cyclicDag2.Dependencies = cyclicDag2Deps + cyclicDagSpec = append(cyclicDagSpec, cyclicDag1, cyclicDag2) + + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(cyclicDagSpec, nil) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + // resolve dependencies + depenResolver := new(mock.DependencyResolver) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, cyclicDagSpec[0], nil).Return(cyclicDagSpec[0], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, cyclicDagSpec[1], nil).Return(cyclicDagSpec[1], nil) + defer depenResolver.AssertExpectations(t) + + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + + jobSvc := job.NewService(nil, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, nil) + replayRequest := &models.ReplayWorkerRequest{ + Job: cyclicDagSpec[0], + Start: replayStart, + End: replayEnd, + Project: projSpec, + } + _, err := jobSvc.ReplayDryRun(replayRequest) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "a cycle dependency encountered in the tree") + }) + + t.Run("resolve create replay tree for a dag with three day task window and mentioned dependencies", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + // resolve dependencies + depenResolver := new(mock.DependencyResolver) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(dagSpec[0], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(dagSpec[3], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(dagSpec[4], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) + defer depenResolver.AssertExpectations(t) + + compiler := new(mock.Compiler) + defer compiler.AssertExpectations(t) + + jobSvc := job.NewService(nil, nil, compiler, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, nil) + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec1], + Start: replayStart, + End: replayEnd, + Project: projSpec, + } + + tree, err := jobSvc.ReplayDryRun(replayRequest) + + assert.Nil(t, err) + countMap := make(map[string][]time.Time) + getRuns(tree, countMap) + expectedRunMap := map[string][]time.Time{} + expectedRunMap[spec1] = []time.Time{ + time.Date(2020, time.Month(8), 5, 2, 0, 0, 0, time.UTC), + time.Date(2020, time.Month(8), 6, 2, 0, 0, 0, time.UTC), + time.Date(2020, time.Month(8), 7, 2, 0, 0, 0, time.UTC), + } + expectedRunMap[spec2] = expectedRunMap[spec1] + expectedRunMap[spec2] = append(expectedRunMap[spec2], time.Date(2020, time.Month(8), 8, 2, 0, 0, 0, time.UTC), time.Date(2020, time.Month(8), 9, 2, 0, 0, 0, time.UTC)) + expectedRunMap[spec3] = expectedRunMap[spec2] + expectedRunMap[spec3] = append(expectedRunMap[spec3], time.Date(2020, time.Month(8), 10, 2, 0, 0, 0, time.UTC), time.Date(2020, time.Month(8), 11, 2, 0, 0, 0, time.UTC)) + for k, v := range countMap { + assert.Equal(t, expectedRunMap[k], v) + } + }) + + t.Run("resolve create replay tree for a dag with three day task window and mentioned dependencies", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + // resolve dependencies + depenResolver := new(mock.DependencyResolver) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(dagSpec[0], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(dagSpec[3], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(dagSpec[4], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) + defer depenResolver.AssertExpectations(t) + + compiler := new(mock.Compiler) + defer compiler.AssertExpectations(t) + + jobSvc := job.NewService(nil, nil, compiler, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, nil) + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec4], + Start: replayStart, + End: replayEnd, + Project: projSpec, + } + + tree, err := jobSvc.ReplayDryRun(replayRequest) + + assert.Nil(t, err) + countMap := make(map[string][]time.Time) + getRuns(tree, countMap) + expectedRunMap := map[string][]time.Time{} + expectedRunMap[spec4] = []time.Time{} + for i := 0; i <= 23; i++ { + expectedRunMap[spec4] = append(expectedRunMap[spec4], time.Date(2020, time.Month(8), 5, i, 0, 0, 0, time.UTC)) + } + expectedRunMap[spec5] = []time.Time{ + time.Date(2020, time.Month(8), 5, 0, 0, 0, 0, time.UTC), + time.Date(2020, time.Month(8), 6, 0, 0, 0, 0, time.UTC), + time.Date(2020, time.Month(8), 7, 0, 0, 0, 0, time.UTC), + time.Date(2020, time.Month(8), 8, 0, 0, 0, 0, time.UTC), + } + expectedRunMap[spec6] = append(expectedRunMap[spec5], time.Date(2020, time.Month(8), 9, 0, 0, 0, 0, time.UTC), time.Date(2020, time.Month(8), 10, 0, 0, 0, 0, time.UTC)) + for k, v := range countMap { + assert.Equal(t, expectedRunMap[k], v) + } + }) }) - t.Run("should fail if unable to resolve jobs using dependency resolver", func(t *testing.T) { - projectJobSpecRepo := new(mock.ProjectJobSpecRepository) - projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) - defer projectJobSpecRepo.AssertExpectations(t) - - projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) - projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) - defer projJobSpecRepoFac.AssertExpectations(t) - - // resolve dependencies - depenResolver := new(mock.DependencyResolver) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(models.JobSpec{}, errors.New("error while fetching dag1")) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(models.JobSpec{}, errors.New("error while fetching dag3")) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(models.JobSpec{}, errors.New("error while fetching dag4")) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) - defer depenResolver.AssertExpectations(t) - - replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") - replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") - - jobSvc := job.NewService(nil, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac) - _, err := jobSvc.ReplayDryRun(namespaceSpec, specs[spec1], replayStart, replayEnd) - - assert.NotNil(t, err) - merr := err.(*multierror.Error) - assert.Equal(t, 3, merr.Len()) - }) - - t.Run("should fail if tree is cyclic", func(t *testing.T) { - cyclicDagSpec := make([]models.JobSpec, 0) - cyclicDag1 := models.JobSpec{Name: "dag1-deps-on-dag2", Schedule: twoAMSchedule, Task: oneDayTaskWindow} - cyclicDag2 := models.JobSpec{Name: "dag2-deps-on-dag1", Schedule: twoAMSchedule, Task: oneDayTaskWindow} - cyclicDag1Deps := make(map[string]models.JobSpecDependency) - cyclicDag1Deps[cyclicDag1.Name] = models.JobSpecDependency{Job: &cyclicDag2} - cyclicDag2Deps := make(map[string]models.JobSpecDependency) - cyclicDag2Deps[cyclicDag2.Name] = models.JobSpecDependency{Job: &cyclicDag1} - cyclicDag1.Dependencies = cyclicDag1Deps - cyclicDag2.Dependencies = cyclicDag2Deps - cyclicDagSpec = append(cyclicDagSpec, cyclicDag1, cyclicDag2) - - projectJobSpecRepo := new(mock.ProjectJobSpecRepository) - projectJobSpecRepo.On("GetAll").Return(cyclicDagSpec, nil) - defer projectJobSpecRepo.AssertExpectations(t) - - projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) - projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) - defer projJobSpecRepoFac.AssertExpectations(t) - - // resolve dependencies - depenResolver := new(mock.DependencyResolver) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, cyclicDagSpec[0], nil).Return(cyclicDagSpec[0], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, cyclicDagSpec[1], nil).Return(cyclicDagSpec[1], nil) - defer depenResolver.AssertExpectations(t) - - replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") - replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") - - jobSvc := job.NewService(nil, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac) - _, err := jobSvc.ReplayDryRun(namespaceSpec, cyclicDagSpec[0], replayStart, replayEnd) - - assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), "a cycle dependency encountered in the tree")) - }) - - t.Run("resolve create replay tree for a dag with three day task window and mentioned dependencies", func(t *testing.T) { - projectJobSpecRepo := new(mock.ProjectJobSpecRepository) - projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) - defer projectJobSpecRepo.AssertExpectations(t) - - projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) - projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) - defer projJobSpecRepoFac.AssertExpectations(t) - - // resolve dependencies - depenResolver := new(mock.DependencyResolver) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(dagSpec[0], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(dagSpec[3], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(dagSpec[4], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) - defer depenResolver.AssertExpectations(t) - - compiler := new(mock.Compiler) - defer compiler.AssertExpectations(t) - - jobSvc := job.NewService(nil, nil, compiler, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac) - replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") - replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") - - tree, err := jobSvc.ReplayDryRun(namespaceSpec, specs[spec1], replayStart, replayEnd) - - assert.Nil(t, err) - countMap := make(map[string][]time.Time) - getRuns(tree, countMap) - expectedRunMap := map[string][]time.Time{} - expectedRunMap[spec1] = []time.Time{ - time.Date(2020, time.Month(8), 5, 2, 0, 0, 0, time.UTC), - time.Date(2020, time.Month(8), 6, 2, 0, 0, 0, time.UTC), - time.Date(2020, time.Month(8), 7, 2, 0, 0, 0, time.UTC), - } - expectedRunMap[spec2] = expectedRunMap[spec1] - expectedRunMap[spec2] = append(expectedRunMap[spec2], time.Date(2020, time.Month(8), 8, 2, 0, 0, 0, time.UTC), time.Date(2020, time.Month(8), 9, 2, 0, 0, 0, time.UTC)) - expectedRunMap[spec3] = expectedRunMap[spec2] - expectedRunMap[spec3] = append(expectedRunMap[spec3], time.Date(2020, time.Month(8), 10, 2, 0, 0, 0, time.UTC), time.Date(2020, time.Month(8), 11, 2, 0, 0, 0, time.UTC)) - for k, v := range countMap { - assert.Equal(t, expectedRunMap[k], v) - } - }) - - t.Run("resolve create replay tree for a dag with three day task window and mentioned dependencies", func(t *testing.T) { - projectJobSpecRepo := new(mock.ProjectJobSpecRepository) - projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) - defer projectJobSpecRepo.AssertExpectations(t) - - projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) - projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) - defer projJobSpecRepoFac.AssertExpectations(t) - - // resolve dependencies - depenResolver := new(mock.DependencyResolver) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(dagSpec[0], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(dagSpec[3], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(dagSpec[4], nil) - depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) - defer depenResolver.AssertExpectations(t) - - compiler := new(mock.Compiler) - defer compiler.AssertExpectations(t) - - jobSvc := job.NewService(nil, nil, compiler, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac) - replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") - replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") - - tree, err := jobSvc.ReplayDryRun(namespaceSpec, specs[spec4], replayStart, replayEnd) - - assert.Nil(t, err) - countMap := make(map[string][]time.Time) - getRuns(tree, countMap) - expectedRunMap := map[string][]time.Time{} - expectedRunMap[spec4] = []time.Time{} - for i := 0; i <= 23; i++ { - expectedRunMap[spec4] = append(expectedRunMap[spec4], time.Date(2020, time.Month(8), 5, i, 0, 0, 0, time.UTC)) - } - expectedRunMap[spec5] = []time.Time{ - time.Date(2020, time.Month(8), 5, 0, 0, 0, 0, time.UTC), - time.Date(2020, time.Month(8), 6, 0, 0, 0, 0, time.UTC), - time.Date(2020, time.Month(8), 7, 0, 0, 0, 0, time.UTC), - time.Date(2020, time.Month(8), 8, 0, 0, 0, 0, time.UTC), - } - expectedRunMap[spec6] = append(expectedRunMap[spec5], time.Date(2020, time.Month(8), 9, 0, 0, 0, 0, time.UTC), time.Date(2020, time.Month(8), 10, 0, 0, 0, 0, time.UTC)) - for k, v := range countMap { - assert.Equal(t, expectedRunMap[k], v) - } + t.Run("Replay", func(t *testing.T) { + t.Run("should fail if unable to fetch jobSpecs from project jobSpecRepo", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(nil, errors.New("error while getting all dags")) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + + jobSvc := job.NewService(nil, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac, nil) + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec1], + Start: replayStart, + End: replayEnd, + Project: projSpec, + } + _, err := jobSvc.Replay(replayRequest) + + assert.NotNil(t, err) + }) + + t.Run("should fail if replay manager throws an error", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + depenResolver := new(mock.DependencyResolver) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(dagSpec[0], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(dagSpec[3], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(dagSpec[4], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) + defer depenResolver.AssertExpectations(t) + + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec1], + Start: replayStart, + End: replayEnd, + Project: projSpec, + JobSpecMap: specs, + } + + errMessage := "error with replay manager" + replayManager := new(mock.ReplayManager) + replayManager.On("Replay", replayRequest).Return("", errors.New(errMessage)) + defer replayManager.AssertExpectations(t) + + jobSvc := job.NewService(nil, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, replayManager) + + _, err := jobSvc.Replay(replayRequest) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), errMessage) + }) + + t.Run("should succeed if replay manager successfully processes request", func(t *testing.T) { + projectJobSpecRepo := new(mock.ProjectJobSpecRepository) + projectJobSpecRepo.On("GetAll").Return(dagSpec, nil) + defer projectJobSpecRepo.AssertExpectations(t) + + projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) + projJobSpecRepoFac.On("New", projSpec).Return(projectJobSpecRepo) + defer projJobSpecRepoFac.AssertExpectations(t) + + depenResolver := new(mock.DependencyResolver) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[0], nil).Return(dagSpec[0], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[1], nil).Return(dagSpec[1], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[2], nil).Return(dagSpec[2], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[3], nil).Return(dagSpec[3], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[4], nil).Return(dagSpec[4], nil) + depenResolver.On("Resolve", projSpec, projectJobSpecRepo, dagSpec[5], nil).Return(dagSpec[5], nil) + defer depenResolver.AssertExpectations(t) + + replayStart, _ := time.Parse(job.ReplayDateFormat, "2020-08-05") + replayEnd, _ := time.Parse(job.ReplayDateFormat, "2020-08-07") + replayRequest := &models.ReplayWorkerRequest{ + Job: specs[spec1], + Start: replayStart, + End: replayEnd, + Project: projSpec, + JobSpecMap: specs, + } + + replayManager := new(mock.ReplayManager) + objUUID := uuid.Must(uuid.NewRandom()) + replayManager.On("Replay", replayRequest).Return(objUUID.String(), nil) + defer replayManager.AssertExpectations(t) + + jobSvc := job.NewService(nil, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, replayManager) + + replayUUID, err := jobSvc.Replay(replayRequest) + assert.Nil(t, err) + assert.Equal(t, objUUID.String(), replayUUID) + }) }) } diff --git a/job/replay_worker.go b/job/replay_worker.go new file mode 100644 index 0000000000..d08e73be2e --- /dev/null +++ b/job/replay_worker.go @@ -0,0 +1,66 @@ +package job + +import ( + "context" + "fmt" + "time" + + "github.com/odpf/optimus/core/logger" + + "github.com/odpf/optimus/models" + "github.com/pkg/errors" +) + +const ( + AirflowClearDagRunFailed = "failed to clear airflow dag run" +) + +type ReplayWorker interface { + Process(context.Context, *models.ReplayWorkerRequest) error +} + +type replayWorker struct { + replaySpecRepoFac ReplaySpecRepoFactory + scheduler models.SchedulerUnit +} + +func (w *replayWorker) Process(ctx context.Context, input *models.ReplayWorkerRequest) (err error) { + replaySpecRepo := w.replaySpecRepoFac.New(input.Job) + // mark replay request in progress + if inProgressErr := replaySpecRepo.UpdateStatus(input.ID, models.ReplayStatusInProgress, models.ReplayMessage{}); inProgressErr != nil { + return inProgressErr + } + + replayTree, err := prepareTree(input) + if err != nil { + return err + } + + replayDagsMap := replayTree.GetAllNodes() + for _, treeNode := range replayDagsMap { + runTimes := treeNode.Runs.Values() + startTime := runTimes[0].(time.Time) + endTime := runTimes[treeNode.Runs.Size()-1].(time.Time) + if err = w.scheduler.Clear(ctx, input.Project, treeNode.GetName(), startTime, endTime); err != nil { + err = errors.Wrapf(err, "error while clearing dag runs for job %s", treeNode.GetName()) + logger.W(fmt.Sprintf("error while running replay %s: %s", input.ID.String(), err.Error())) + if updateStatusErr := replaySpecRepo.UpdateStatus(input.ID, models.ReplayStatusFailed, models.ReplayMessage{ + Type: AirflowClearDagRunFailed, + Message: err.Error(), + }); updateStatusErr != nil { + return updateStatusErr + } + return err + } + } + + if err = replaySpecRepo.UpdateStatus(input.ID, models.ReplayStatusSuccess, models.ReplayMessage{}); err != nil { + return err + } + logger.I(fmt.Sprintf("successfully completed replay id: %s", input.ID.String())) + return nil +} + +func NewReplayWorker(replaySpecRepoFac ReplaySpecRepoFactory, scheduler models.SchedulerUnit) *replayWorker { + return &replayWorker{replaySpecRepoFac: replaySpecRepoFac, scheduler: scheduler} +} diff --git a/job/replay_worker_test.go b/job/replay_worker_test.go new file mode 100644 index 0000000000..c466dbe90c --- /dev/null +++ b/job/replay_worker_test.go @@ -0,0 +1,157 @@ +package job_test + +import ( + "context" + "io/ioutil" + "testing" + "time" + + "github.com/odpf/optimus/core/logger" + + "github.com/google/uuid" + "github.com/odpf/optimus/job" + "github.com/odpf/optimus/mock" + "github.com/odpf/optimus/models" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestReplayWorker(t *testing.T) { + logger.InitWithWriter(logger.DEBUG, ioutil.Discard) + dagStartTime, _ := time.Parse(job.ReplayDateFormat, "2020-04-05") + startDate, _ := time.Parse(job.ReplayDateFormat, "2020-08-22") + endDate, _ := time.Parse(job.ReplayDateFormat, "2020-08-26") + currUUID := uuid.Must(uuid.NewRandom()) + dagRunStartTime := time.Date(2020, time.Month(8), 22, 2, 0, 0, 0, time.UTC) + dagRunEndTime := time.Date(2020, time.Month(8), 26, 2, 0, 0, 0, time.UTC) + jobSpec := models.JobSpec{ + Name: "job-name", + Schedule: models.JobSpecSchedule{ + StartDate: dagStartTime, + Interval: "0 2 * * *", + }, + } + replayRequest := &models.ReplayWorkerRequest{ + ID: currUUID, + Job: jobSpec, + Start: startDate, + End: endDate, + Project: models.ProjectSpec{ + Name: "project-name", + }, + JobSpecMap: map[string]models.JobSpec{ + "job-name": jobSpec, + }, + } + t.Run("Process", func(t *testing.T) { + t.Run("should throw an error when replayRepo throws an error", func(t *testing.T) { + ctx := context.Background() + replayRepository := new(mock.ReplayRepository) + defer replayRepository.AssertExpectations(t) + errMessage := "replay repo error" + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusInProgress, models.ReplayMessage{}).Return(errors.New(errMessage)) + + replaySpecRepoFac := new(mock.ReplaySpecRepoFactory) + defer replaySpecRepoFac.AssertExpectations(t) + replaySpecRepoFac.On("New", replayRequest.Job).Return(replayRepository) + + worker := job.NewReplayWorker(replaySpecRepoFac, nil) + err := worker.Process(ctx, replayRequest) + assert.NotNil(t, err) + assert.Equal(t, errMessage, err.Error()) + }) + t.Run("should throw an error when scheduler throws an error", func(t *testing.T) { + ctx := context.Background() + replayRepository := new(mock.ReplayRepository) + defer replayRepository.AssertExpectations(t) + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusInProgress, models.ReplayMessage{}).Return(nil) + errMessage := "error while clearing dag runs for job job-name: scheduler clear error" + failedReplayMessage := models.ReplayMessage{ + Type: job.AirflowClearDagRunFailed, + Message: errMessage, + } + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusFailed, failedReplayMessage).Return(nil) + + replaySpecRepoFac := new(mock.ReplaySpecRepoFactory) + defer replaySpecRepoFac.AssertExpectations(t) + replaySpecRepoFac.On("New", replayRequest.Job).Return(replayRepository) + + scheduler := new(mock.MockScheduler) + defer scheduler.AssertExpectations(t) + errorMessage := "scheduler clear error" + scheduler.On("Clear", ctx, replayRequest.Project, "job-name", dagRunStartTime, dagRunEndTime).Return(errors.New(errorMessage)) + + worker := job.NewReplayWorker(replaySpecRepoFac, scheduler) + err := worker.Process(ctx, replayRequest) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), errorMessage) + }) + t.Run("should throw an error when updatestatus throws an error for failed request", func(t *testing.T) { + ctx := context.Background() + replayRepository := new(mock.ReplayRepository) + defer replayRepository.AssertExpectations(t) + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusInProgress, models.ReplayMessage{}).Return(nil) + errMessage := "error while clearing dag runs for job job-name: scheduler clear error" + failedReplayMessage := models.ReplayMessage{ + Type: job.AirflowClearDagRunFailed, + Message: errMessage, + } + updateStatusErr := errors.New("error while updating status to failed") + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusFailed, failedReplayMessage).Return(updateStatusErr) + + replaySpecRepoFac := new(mock.ReplaySpecRepoFactory) + defer replaySpecRepoFac.AssertExpectations(t) + replaySpecRepoFac.On("New", replayRequest.Job).Return(replayRepository) + + scheduler := new(mock.MockScheduler) + defer scheduler.AssertExpectations(t) + errorMessage := "scheduler clear error" + scheduler.On("Clear", ctx, replayRequest.Project, "job-name", dagRunStartTime, dagRunEndTime).Return(errors.New(errorMessage)) + + worker := job.NewReplayWorker(replaySpecRepoFac, scheduler) + err := worker.Process(ctx, replayRequest) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), updateStatusErr.Error()) + }) + + t.Run("should throw an error when updatestatus throws an error for successful request", func(t *testing.T) { + ctx := context.Background() + replayRepository := new(mock.ReplayRepository) + defer replayRepository.AssertExpectations(t) + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusInProgress, models.ReplayMessage{}).Return(nil) + updateSuccessStatusErr := errors.New("error while updating replay request") + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusSuccess, models.ReplayMessage{}).Return(updateSuccessStatusErr) + + replaySpecRepoFac := new(mock.ReplaySpecRepoFactory) + defer replaySpecRepoFac.AssertExpectations(t) + replaySpecRepoFac.On("New", replayRequest.Job).Return(replayRepository) + + scheduler := new(mock.MockScheduler) + defer scheduler.AssertExpectations(t) + scheduler.On("Clear", ctx, replayRequest.Project, "job-name", dagRunStartTime, dagRunEndTime).Return(nil) + + worker := job.NewReplayWorker(replaySpecRepoFac, scheduler) + err := worker.Process(ctx, replayRequest) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), updateSuccessStatusErr.Error()) + }) + t.Run("should update replay status if successful", func(t *testing.T) { + ctx := context.Background() + replayRepository := new(mock.ReplayRepository) + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusInProgress, models.ReplayMessage{}).Return(nil) + replayRepository.On("UpdateStatus", currUUID, models.ReplayStatusSuccess, models.ReplayMessage{}).Return(nil) + + replaySpecRepoFac := new(mock.ReplaySpecRepoFactory) + defer replaySpecRepoFac.AssertExpectations(t) + replaySpecRepoFac.On("New", replayRequest.Job).Return(replayRepository) + + scheduler := new(mock.MockScheduler) + defer scheduler.AssertExpectations(t) + scheduler.On("Clear", ctx, replayRequest.Project, "job-name", dagRunStartTime, dagRunEndTime).Return(nil) + + worker := job.NewReplayWorker(replaySpecRepoFac, scheduler) + err := worker.Process(ctx, replayRequest) + assert.Nil(t, err) + }) + }) +} diff --git a/job/service.go b/job/service.go index b4d020545d..20528eff68 100644 --- a/job/service.go +++ b/job/service.go @@ -52,6 +52,11 @@ type JobRepoFactory interface { New(context.Context, models.ProjectSpec) (store.JobRepository, error) } +// ReplaySpecRepoFactory is used to manage replay spec objects from store +type ReplaySpecRepoFactory interface { + New(jobSpec models.JobSpec) store.ReplaySpecRepository +} + // Service compiles all jobs with its dependencies, priority and // and other properties. Finally, it syncs the jobs with corresponding // store @@ -63,6 +68,7 @@ type Service struct { priorityResolver PriorityResolver metaSvcFactory meta.MetaSvcFactory projectJobSpecRepoFactory ProjectJobSpecRepoFactory + replayManager ReplayManager Now func() time.Time assetCompiler AssetCompiler @@ -471,6 +477,7 @@ func NewService(jobSpecRepoFactory SpecRepoFactory, jobRepoFact JobRepoFactory, compiler models.JobCompiler, assetCompiler AssetCompiler, dependencyResolver DependencyResolver, priorityResolver PriorityResolver, metaSvcFactory meta.MetaSvcFactory, projectJobSpecRepoFactory ProjectJobSpecRepoFactory, + replayManager ReplayManager, ) *Service { return &Service{ jobSpecRepoFactory: jobSpecRepoFactory, @@ -480,6 +487,7 @@ func NewService(jobSpecRepoFactory SpecRepoFactory, jobRepoFact JobRepoFactory, priorityResolver: priorityResolver, metaSvcFactory: metaSvcFactory, projectJobSpecRepoFactory: projectJobSpecRepoFactory, + replayManager: replayManager, assetCompiler: assetCompiler, Now: time.Now, diff --git a/job/service_test.go b/job/service_test.go index 5445f26a9f..90c76a244d 100644 --- a/job/service_test.go +++ b/job/service_test.go @@ -2,7 +2,6 @@ package job_test import ( "context" - "strings" "testing" "time" @@ -54,7 +53,7 @@ func TestService(t *testing.T) { projJobSpecRepoFac := new(mock.ProjectJobSpecRepoFactory) defer projJobSpecRepoFac.AssertExpectations(t) - svc := job.NewService(repoFac, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac) + svc := job.NewService(repoFac, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac, nil) err := svc.Create(namespaceSpec, jobSpec) assert.Nil(t, err) }) @@ -86,7 +85,7 @@ func TestService(t *testing.T) { repoFac.On("New", namespaceSpec).Return(repo) defer repoFac.AssertExpectations(t) - svc := job.NewService(repoFac, nil, nil, dumpAssets, nil, nil, nil, nil) + svc := job.NewService(repoFac, nil, nil, dumpAssets, nil, nil, nil, nil, nil) err := svc.Create(namespaceSpec, jobSpec) assert.NotNil(t, err) }) @@ -195,7 +194,7 @@ func TestService(t *testing.T) { jobRepo.On("Save", ctx, compiledJob).Return(nil) } - svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac, nil) err := svc.Sync(ctx, namespaceSpec, nil) assert.Nil(t, err) }) @@ -311,7 +310,7 @@ func TestService(t *testing.T) { // delete unwanted jobRepo.On("Delete", ctx, namespaceSpec, jobs[1].Name).Return(nil) - svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac, nil) err := svc.Sync(ctx, namespaceSpec, nil) assert.Nil(t, err) }) @@ -359,12 +358,12 @@ func TestService(t *testing.T) { errors.New("error test-2")) defer depenResolver.AssertExpectations(t) - svc := job.NewService(jobSpecRepoFac, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, nil, nil, dumpAssets, depenResolver, nil, nil, projJobSpecRepoFac, nil) err := svc.Sync(ctx, namespaceSpec, nil) assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), "2 errors occurred")) - assert.True(t, strings.Contains(err.Error(), "error test")) - assert.True(t, strings.Contains(err.Error(), "error test-2")) + assert.Contains(t, err.Error(), "2 errors occurred") + assert.Contains(t, err.Error(), "error test") + assert.Contains(t, err.Error(), "error test-2") }) t.Run("should successfully publish metadata for all job specs", func(t *testing.T) { @@ -467,7 +466,7 @@ func TestService(t *testing.T) { jobRepo.On("Save", ctx, compiledJob).Return(nil) } - svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, metaSvcFact, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, metaSvcFact, projJobSpecRepoFac, nil) err := svc.Sync(ctx, namespaceSpec, nil) assert.Nil(t, err) }) @@ -541,7 +540,7 @@ func TestService(t *testing.T) { // delete unwanted jobSpecRepo.On("Delete", jobSpecsBase[0].Name).Return(nil) - svc := job.NewService(jobSpecRepoFac, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, nil, nil, dumpAssets, nil, nil, nil, projJobSpecRepoFac, nil) err := svc.KeepOnly(namespaceSpec, toKeep, nil) assert.Nil(t, err) }) @@ -646,7 +645,7 @@ func TestService(t *testing.T) { compiler.On("Compile", namespaceSpec, jobSpecsAfterPriorityResolve[idx]).Return(compiledJob, nil) } - svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac, nil) compiledJob, err := svc.Dump(namespaceSpec, jobSpecsBase[0]) assert.Nil(t, err) assert.Equal(t, "come string", string(compiledJob.Contents)) @@ -758,7 +757,7 @@ func TestService(t *testing.T) { jobRepo.On("Save", ctx, compiledJob).Return(nil) } - svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac, nil) err := svc.Delete(ctx, namespaceSpec, jobSpecsBase[0]) assert.Nil(t, err) }) @@ -847,7 +846,7 @@ func TestService(t *testing.T) { compiler := new(mock.Compiler) defer compiler.AssertExpectations(t) - svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac) + svc := job.NewService(jobSpecRepoFac, jobRepoFac, compiler, dumpAssets, depenResolver, priorityResolver, nil, projJobSpecRepoFac, nil) err := svc.Delete(ctx, namespaceSpec, jobSpecsBase[0]) assert.NotNil(t, err) assert.Equal(t, "cannot delete job test since it's dependency of job downstream-test", err.Error()) diff --git a/mock/job.go b/mock/job.go index f659228f7a..f4ec37a790 100644 --- a/mock/job.go +++ b/mock/job.go @@ -2,7 +2,6 @@ package mock import ( "context" - "time" "github.com/odpf/optimus/job" @@ -197,11 +196,16 @@ func (j *JobService) Delete(ctx context.Context, c models.NamespaceSpec, job mod return args.Error(0) } -func (j *JobService) ReplayDryRun(namespace models.NamespaceSpec, jobSpec models.JobSpec, start time.Time, end time.Time) (*tree.TreeNode, error) { - args := j.Called(namespace, jobSpec, start, end) +func (j *JobService) ReplayDryRun(replayRequest *models.ReplayWorkerRequest) (*tree.TreeNode, error) { + args := j.Called(replayRequest) return args.Get(0).(*tree.TreeNode), args.Error(1) } +func (j *JobService) Replay(replayRequest *models.ReplayWorkerRequest) (string, error) { + args := j.Called(replayRequest) + return args.Get(0).(string), args.Error(1) +} + type Compiler struct { mock.Mock } diff --git a/mock/replay.go b/mock/replay.go new file mode 100644 index 0000000000..a9342f071c --- /dev/null +++ b/mock/replay.go @@ -0,0 +1,58 @@ +package mock + +import ( + "context" + + "github.com/google/uuid" + "github.com/odpf/optimus/models" + "github.com/odpf/optimus/store" + "github.com/stretchr/testify/mock" +) + +type ReplayRepository struct { + mock.Mock +} + +func (repo *ReplayRepository) GetByID(id uuid.UUID) (models.ReplaySpec, error) { + args := repo.Called(id) + return args.Get(0).(models.ReplaySpec), args.Error(1) +} + +func (repo *ReplayRepository) Insert(replay *models.ReplaySpec) error { + return repo.Called(replay).Error(0) +} + +func (repo *ReplayRepository) UpdateStatus(replayID uuid.UUID, status string, message models.ReplayMessage) error { + return repo.Called(replayID, status, message).Error(0) +} + +type ReplaySpecRepoFactory struct { + mock.Mock +} + +func (fac *ReplaySpecRepoFactory) New(jobSpec models.JobSpec) store.ReplaySpecRepository { + return fac.Called(jobSpec).Get(0).(store.ReplaySpecRepository) +} + +type ReplayManager struct { + mock.Mock +} + +func (rm *ReplayManager) Replay(reqInput *models.ReplayWorkerRequest) (string, error) { + args := rm.Called(reqInput) + return args.Get(0).(string), args.Error(1) +} + +func (rm *ReplayManager) Init() { + rm.Called() + return +} + +type ReplayWorker struct { + mock.Mock +} + +func (rm *ReplayWorker) Process(ctx context.Context, replayRequest *models.ReplayWorkerRequest) error { + args := rm.Called(ctx, replayRequest) + return args.Error(0) +} diff --git a/mock/scheduler.go b/mock/scheduler.go new file mode 100644 index 0000000000..641e6c70c1 --- /dev/null +++ b/mock/scheduler.go @@ -0,0 +1,43 @@ +package mock + +import ( + "context" + "time" + + "github.com/odpf/optimus/models" + "github.com/stretchr/testify/mock" +) + +type MockScheduler struct { + mock.Mock +} + +func (ms *MockScheduler) GetName() string { + return "" +} + +func (ms *MockScheduler) GetTemplate() []byte { + return []byte{} +} + +func (ms *MockScheduler) GetJobsDir() string { + return "" +} + +func (ms *MockScheduler) GetJobsExtension() string { + return "" +} + +func (ms *MockScheduler) Bootstrap(ctx context.Context, projectSpec models.ProjectSpec) error { + return ms.Called(ctx, projectSpec).Error(0) +} + +func (ms *MockScheduler) GetJobStatus(ctx context.Context, projSpec models.ProjectSpec, jobName string) ([]models.JobStatus, error) { + args := ms.Called(ctx, projSpec, jobName) + return args.Get(0).([]models.JobStatus), args.Error(1) +} + +func (ms *MockScheduler) Clear(ctx context.Context, projSpec models.ProjectSpec, jobName string, startDate, endDate time.Time) error { + args := ms.Called(ctx, projSpec, jobName, startDate, endDate) + return args.Error(0) +} diff --git a/mock/uuid.go b/mock/uuid.go new file mode 100644 index 0000000000..62b6648cb5 --- /dev/null +++ b/mock/uuid.go @@ -0,0 +1,15 @@ +package mock + +import ( + "github.com/google/uuid" + "github.com/stretchr/testify/mock" +) + +type UUIDProvider struct { + mock.Mock +} + +func (up *UUIDProvider) NewUUID() (uuid.UUID, error) { + args := up.Called() + return args.Get(0).(uuid.UUID), args.Error(1) +} diff --git a/models/job.go b/models/job.go index 025316c69f..e9220947c3 100644 --- a/models/job.go +++ b/models/job.go @@ -292,8 +292,10 @@ type JobService interface { GetByName(string, NamespaceSpec) (JobSpec, error) // Dump returns the compiled Job Dump(NamespaceSpec, JobSpec) (Job, error) - // ReplayDryRun replays the jobSpec and its dependencies between start and endDate - ReplayDryRun(NamespaceSpec, JobSpec, time.Time, time.Time) (*tree.TreeNode, error) + // ReplayDryRun returns the execution tree of jobSpec and its dependencies between start and endDate + ReplayDryRun(*ReplayWorkerRequest) (*tree.TreeNode, error) + // Replay replays the jobSpec and its dependencies between start and endDate + Replay(*ReplayWorkerRequest) (string, error) // KeepOnly deletes all jobs except the ones provided for a namespace KeepOnly(NamespaceSpec, []JobSpec, progress.Observer) error // GetAll reads all job specifications of the given namespace diff --git a/models/replay.go b/models/replay.go new file mode 100644 index 0000000000..6f68f2ad4f --- /dev/null +++ b/models/replay.go @@ -0,0 +1,39 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +const ( + // ReplayStatusAccepted worker picked up the request + ReplayStatusAccepted = "accepted" + ReplayStatusInProgress = "inprogress" + // ReplayStatusFailed worker fail while processing the replay request + ReplayStatusFailed = "failed" // end state + ReplayStatusSuccess = "success" // end state +) + +type ReplayMessage struct { + Type string + Message string +} + +type ReplayWorkerRequest struct { + ID uuid.UUID + Job JobSpec + Start time.Time + End time.Time + Project ProjectSpec + JobSpecMap map[string]JobSpec +} + +type ReplaySpec struct { + ID uuid.UUID + Job JobSpec + StartDate time.Time + EndDate time.Time + Status string + Message ReplayMessage +} diff --git a/models/scheduler.go b/models/scheduler.go index 2546730398..99f0be6575 100644 --- a/models/scheduler.go +++ b/models/scheduler.go @@ -37,6 +37,9 @@ type SchedulerUnit interface { // GetJobStatus should return the current and previous status of job GetJobStatus(ctx context.Context, projSpec ProjectSpec, jobName string) ([]JobStatus, error) + + // Clear clears state of job between provided start and end dates + Clear(ctx context.Context, projSpec ProjectSpec, jobName string, startDate, endDate time.Time) error } type JobStatusState string diff --git a/store/gcs/job_repository_test.go b/store/gcs/job_repository_test.go index cdf7b8e409..0ba5045486 100644 --- a/store/gcs/job_repository_test.go +++ b/store/gcs/job_repository_test.go @@ -197,8 +197,8 @@ func TestJobRepository(t *testing.T) { Prefix: prefix, } err := repo.Delete(ctx, namespaceSpec, jobName) - - assert.Error(t, models.ErrNoSuchJob, err) + assert.Error(t, err) + assert.Contains(t, err.Error(), models.ErrNoSuchJob.Error()) }) t.Run("should return err when unable to get the object info", func(t *testing.T) { namespaceSpec := models.NamespaceSpec{ @@ -366,8 +366,8 @@ func TestJobRepository(t *testing.T) { Suffix: suffix, } _, err := repo.GetByName(ctx, nonExistentDAGName) - - assert.Error(t, models.ErrNoSuchJob, err) + assert.Error(t, err) + assert.Contains(t, err.Error(), models.ErrNoSuchJob.Error()) }) t.Run("should return error when failed to get the bucket", func(t *testing.T) { expected := errors.New("failed to get bucket attrs") diff --git a/store/postgres/migrations/000010_create_replay_table.down.sql b/store/postgres/migrations/000010_create_replay_table.down.sql new file mode 100644 index 0000000000..ee9125596c --- /dev/null +++ b/store/postgres/migrations/000010_create_replay_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS replay; diff --git a/store/postgres/migrations/000010_create_replay_table.up.sql b/store/postgres/migrations/000010_create_replay_table.up.sql new file mode 100644 index 0000000000..220fe6f5a5 --- /dev/null +++ b/store/postgres/migrations/000010_create_replay_table.up.sql @@ -0,0 +1,11 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE TABLE IF NOT EXISTS replay ( + id UUID PRIMARY KEY NOT NULL, + job_id UUID NOT NULL, + start_date TIMESTAMP WITH TIME ZONE NOT NULL, + end_date TIMESTAMP WITH TIME ZONE NOT NULL, + status varchar(30) NOT NULL, + message JSONB, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL +); diff --git a/store/postgres/replay_repository.go b/store/postgres/replay_repository.go new file mode 100644 index 0000000000..0d7669bb07 --- /dev/null +++ b/store/postgres/replay_repository.go @@ -0,0 +1,104 @@ +package postgres + +import ( + "encoding/json" + "errors" + "time" + + "gorm.io/datatypes" + + "github.com/google/uuid" + "github.com/jinzhu/gorm" + "github.com/odpf/optimus/models" + "github.com/odpf/optimus/store" +) + +type Replay struct { + ID uuid.UUID `gorm:"primary_key;type:uuid"` + + JobID uuid.UUID `gorm:"not null"` + Job Job `gorm:"foreignKey:JobID"` + + StartDate time.Time `gorm:"not null"` + EndDate time.Time `gorm:"not null"` + Status string `gorm:"not null"` + Message datatypes.JSON + + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` +} + +func (p Replay) FromSpec(spec *models.ReplaySpec) (Replay, error) { + jsonBytes, err := json.Marshal(spec.Message) + if err != nil { + return Replay{}, nil + } + return Replay{ + ID: spec.ID, + JobID: spec.Job.ID, + StartDate: spec.StartDate, + EndDate: spec.EndDate, + Status: spec.Status, + Message: jsonBytes, + }, nil +} + +func (p Replay) ToSpec(jobSpec models.JobSpec) (models.ReplaySpec, error) { + message := models.ReplayMessage{} + if err := json.Unmarshal(p.Message, &message); err != nil { + return models.ReplaySpec{}, nil + } + return models.ReplaySpec{ + ID: p.ID, + Job: jobSpec, + Status: p.Status, + StartDate: p.StartDate, + EndDate: p.EndDate, + Message: message, + }, nil +} + +type replayRepository struct { + DB *gorm.DB + jobSpec models.JobSpec +} + +func NewReplayRepository(db *gorm.DB, jobSpec models.JobSpec) *replayRepository { + return &replayRepository{ + DB: db, + jobSpec: jobSpec, + } +} + +func (repo *replayRepository) Insert(replay *models.ReplaySpec) error { + r, err := Replay{}.FromSpec(replay) + if err != nil { + return err + } + return repo.DB.Create(&r).Error +} + +func (repo *replayRepository) GetByID(id uuid.UUID) (models.ReplaySpec, error) { + var r Replay + if err := repo.DB.Where("id = ?", id).Find(&r).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return models.ReplaySpec{}, store.ErrResourceNotFound + } + return models.ReplaySpec{}, err + } + return r.ToSpec(repo.jobSpec) +} + +func (repo *replayRepository) UpdateStatus(replayID uuid.UUID, status string, message models.ReplayMessage) error { + var r Replay + if err := repo.DB.Where("id = ?", replayID).Find(&r).Error; err != nil { + return errors.New("could not update non-existing replay") + } + jsonBytes, err := json.Marshal(message) + if err != nil { + return err + } + r.Status = status + r.Message = jsonBytes + return repo.DB.Save(&r).Error +} diff --git a/store/postgres/replay_repository_test.go b/store/postgres/replay_repository_test.go new file mode 100644 index 0000000000..84ef2dd474 --- /dev/null +++ b/store/postgres/replay_repository_test.go @@ -0,0 +1,98 @@ +// +build !unit_test + +package postgres + +import ( + "os" + "testing" + "time" + + "github.com/google/uuid" + "github.com/jinzhu/gorm" + "github.com/odpf/optimus/job" + "github.com/odpf/optimus/models" + "github.com/stretchr/testify/assert" +) + +func TestReplayRepository(t *testing.T) { + DBSetup := func() *gorm.DB { + dbURL, ok := os.LookupEnv("TEST_OPTIMUS_DB_URL") + if !ok { + panic("unable to find TEST_OPTIMUS_DB_URL env var") + } + dbConn, err := Connect(dbURL, 1, 1) + if err != nil { + panic(err) + } + m, err := NewHTTPFSMigrator(dbURL) + if err != nil { + panic(err) + } + if err := m.Drop(); err != nil { + panic(err) + } + if err := Migrate(dbURL); err != nil { + panic(err) + } + + return dbConn + } + + jobSpec := models.JobSpec{ + Name: "job-name", + } + + startTime, _ := time.Parse(job.ReplayDateFormat, "2020-01-15") + endTime, _ := time.Parse(job.ReplayDateFormat, "2020-01-20") + uuid := uuid.Must(uuid.NewRandom()) + testConfigs := []*models.ReplaySpec{ + { + ID: uuid, + Job: jobSpec, + StartDate: startTime, + EndDate: endTime, + Status: models.ReplayStatusAccepted, + }, + } + + t.Run("Insert and GetByID", func(t *testing.T) { + db := DBSetup() + defer db.Close() + testModels := []*models.ReplaySpec{} + testModels = append(testModels, testConfigs...) + + repo := NewReplayRepository(db, jobSpec) + + err := repo.Insert(testModels[0]) + assert.Nil(t, err) + + checkModel, err := repo.GetByID(testModels[0].ID) + assert.Nil(t, err) + assert.Equal(t, uuid, checkModel.ID) + }) + + t.Run("UpdateStatus", func(t *testing.T) { + db := DBSetup() + defer db.Close() + testModels := []*models.ReplaySpec{} + testModels = append(testModels, testConfigs...) + + repo := NewReplayRepository(db, jobSpec) + + err := repo.Insert(testModels[0]) + assert.Nil(t, err) + + errMessage := "failed to execute" + replayMessage := models.ReplayMessage{ + Type: "test failure", + Message: errMessage, + } + err = repo.UpdateStatus(uuid, models.ReplayStatusFailed, replayMessage) + assert.Nil(t, err) + + checkModel, err := repo.GetByID(testModels[0].ID) + assert.Nil(t, err) + assert.Equal(t, models.ReplayStatusFailed, checkModel.Status) + assert.Equal(t, errMessage, checkModel.Message.Message) + }) +} diff --git a/store/store.go b/store/store.go index 3294c29a16..de0183001f 100644 --- a/store/store.go +++ b/store/store.go @@ -6,6 +6,8 @@ import ( "io" "time" + "github.com/google/uuid" + "github.com/odpf/optimus/models" ) @@ -85,3 +87,10 @@ type ObjectWriter interface { type ObjectReader interface { NewReader(bucket, path string) (io.ReadCloser, error) } + +// ReplaySpecRepository represents a storage interface for replay objects +type ReplaySpecRepository interface { + Insert(replay *models.ReplaySpec) error + GetByID(id uuid.UUID) (models.ReplaySpec, error) + UpdateStatus(replayID uuid.UUID, status string, message models.ReplayMessage) error +} diff --git a/third_party/OpenAPI/odpf/optimus/runtime_service.swagger.json b/third_party/OpenAPI/odpf/optimus/runtime_service.swagger.json index 91b25ac19d..20a6b82e45 100644 --- a/third_party/OpenAPI/odpf/optimus/runtime_service.swagger.json +++ b/third_party/OpenAPI/odpf/optimus/runtime_service.swagger.json @@ -229,6 +229,42 @@ ] } }, + "/api/v1/project/{projectName}/job/{jobName}/replay": { + "post": { + "operationId": "RuntimeService_Replay", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/optimusReplayResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "projectName", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "jobName", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "RuntimeService" + ] + } + }, "/api/v1/project/{projectName}/job/{jobName}/replay-dry-run": { "get": { "operationId": "RuntimeService_ReplayDryRun", @@ -1476,6 +1512,14 @@ } } }, + "optimusReplayResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, "optimusResourceSpecification": { "type": "object", "properties": { diff --git a/utils/convert_test.go b/utils/convert_test.go index 5fff17538c..b9be4ec99d 100644 --- a/utils/convert_test.go +++ b/utils/convert_test.go @@ -1,7 +1,6 @@ package utils_test import ( - "strings" "testing" "github.com/AlecAivazis/survey/v2" @@ -31,6 +30,6 @@ func TestConvert(t *testing.T) { } _, err := utils.ConvertToStringMap(inputs) assert.NotNil(t, err) - assert.True(t, strings.Contains(err.Error(), "unknown type found while parsing user inputs")) + assert.Contains(t, err.Error(), "unknown type found while parsing user inputs") }) } diff --git a/utils/uuid.go b/utils/uuid.go new file mode 100644 index 0000000000..63cd76a009 --- /dev/null +++ b/utils/uuid.go @@ -0,0 +1,18 @@ +package utils + +import "github.com/google/uuid" + +type UUIDProvider interface { + NewUUID() (uuid.UUID, error) +} + +type uuidProvider struct { +} + +func (*uuidProvider) NewUUID() (uuid.UUID, error) { + return uuid.NewRandom() +} + +func NewUUIDProvider() *uuidProvider { + return &uuidProvider{} +}