feat: support task cancellation

This commit is contained in:
Gareth George
2023-12-20 08:54:45 +00:00
parent 95ca96a31f
commit fc9c06df00
15 changed files with 366 additions and 74 deletions
+69 -6
View File
@@ -114,6 +114,53 @@ func (x *StringList) GetValues() []string {
return nil
}
type Int64Value struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Value int64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Int64Value) Reset() {
*x = Int64Value{}
if protoimpl.UnsafeEnabled {
mi := &file_types_value_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Int64Value) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Int64Value) ProtoMessage() {}
func (x *Int64Value) ProtoReflect() protoreflect.Message {
mi := &file_types_value_proto_msgTypes[2]
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 Int64Value.ProtoReflect.Descriptor instead.
func (*Int64Value) Descriptor() ([]byte, []int) {
return file_types_value_proto_rawDescGZIP(), []int{2}
}
func (x *Int64Value) GetValue() int64 {
if x != nil {
return x.Value
}
return 0
}
var File_types_value_proto protoreflect.FileDescriptor
var file_types_value_proto_rawDesc = []byte{
@@ -123,10 +170,13 @@ var file_types_value_proto_rawDesc = []byte{
0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,
0x24, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x0a,
0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65,
0x2f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f,
0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x22, 0x0a, 0x0a, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65,
0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x2f, 0x67, 0x65, 0x6e,
0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (
@@ -141,10 +191,11 @@ func file_types_value_proto_rawDescGZIP() []byte {
return file_types_value_proto_rawDescData
}
var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_types_value_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_types_value_proto_goTypes = []interface{}{
(*StringValue)(nil), // 0: types.StringValue
(*StringList)(nil), // 1: types.StringList
(*Int64Value)(nil), // 2: types.Int64Value
}
var file_types_value_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
@@ -184,6 +235,18 @@ func file_types_value_proto_init() {
return nil
}
}
file_types_value_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Int64Value); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -191,7 +254,7 @@ func file_types_value_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_types_value_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
+30 -22
View File
@@ -534,7 +534,7 @@ var file_v1_service_proto_rawDesc = []byte{
0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0xcc, 0x08, 0x0a,
0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0x9c, 0x09, 0x0a,
0x07, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x12, 0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e,
@@ -597,7 +597,12 @@ var file_v1_service_proto_rawDesc = []byte{
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e,
0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x5b,
0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x75, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x4e,
0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73,
0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e,
0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x5b,
0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53,
@@ -634,10 +639,11 @@ var file_v1_service_proto_goTypes = []interface{}{
(*Config)(nil), // 7: v1.Config
(*Repo)(nil), // 8: v1.Repo
(*types.StringValue)(nil), // 9: types.StringValue
(*OperationEvent)(nil), // 10: v1.OperationEvent
(*OperationList)(nil), // 11: v1.OperationList
(*ResticSnapshotList)(nil), // 12: v1.ResticSnapshotList
(*types.StringList)(nil), // 13: types.StringList
(*types.Int64Value)(nil), // 10: types.Int64Value
(*OperationEvent)(nil), // 11: v1.OperationEvent
(*OperationList)(nil), // 12: v1.OperationList
(*ResticSnapshotList)(nil), // 13: v1.ResticSnapshotList
(*types.StringList)(nil), // 14: types.StringList
}
var file_v1_service_proto_depIdxs = []int32{
5, // 0: v1.ListSnapshotFilesResponse.entries:type_name -> v1.LsEntry
@@ -653,22 +659,24 @@ var file_v1_service_proto_depIdxs = []int32{
9, // 10: v1.Restora.Forget:input_type -> types.StringValue
2, // 11: v1.Restora.Restore:input_type -> v1.RestoreSnapshotRequest
9, // 12: v1.Restora.Unlock:input_type -> types.StringValue
9, // 13: v1.Restora.PathAutocomplete:input_type -> types.StringValue
7, // 14: v1.Restora.GetConfig:output_type -> v1.Config
7, // 15: v1.Restora.SetConfig:output_type -> v1.Config
7, // 16: v1.Restora.AddRepo:output_type -> v1.Config
10, // 17: v1.Restora.GetOperationEvents:output_type -> v1.OperationEvent
11, // 18: v1.Restora.GetOperations:output_type -> v1.OperationList
12, // 19: v1.Restora.ListSnapshots:output_type -> v1.ResticSnapshotList
4, // 20: v1.Restora.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
6, // 21: v1.Restora.Backup:output_type -> google.protobuf.Empty
6, // 22: v1.Restora.Prune:output_type -> google.protobuf.Empty
6, // 23: v1.Restora.Forget:output_type -> google.protobuf.Empty
6, // 24: v1.Restora.Restore:output_type -> google.protobuf.Empty
6, // 25: v1.Restora.Unlock:output_type -> google.protobuf.Empty
13, // 26: v1.Restora.PathAutocomplete:output_type -> types.StringList
14, // [14:27] is the sub-list for method output_type
1, // [1:14] is the sub-list for method input_type
10, // 13: v1.Restora.Cancel:input_type -> types.Int64Value
9, // 14: v1.Restora.PathAutocomplete:input_type -> types.StringValue
7, // 15: v1.Restora.GetConfig:output_type -> v1.Config
7, // 16: v1.Restora.SetConfig:output_type -> v1.Config
7, // 17: v1.Restora.AddRepo:output_type -> v1.Config
11, // 18: v1.Restora.GetOperationEvents:output_type -> v1.OperationEvent
12, // 19: v1.Restora.GetOperations:output_type -> v1.OperationList
13, // 20: v1.Restora.ListSnapshots:output_type -> v1.ResticSnapshotList
4, // 21: v1.Restora.ListSnapshotFiles:output_type -> v1.ListSnapshotFilesResponse
6, // 22: v1.Restora.Backup:output_type -> google.protobuf.Empty
6, // 23: v1.Restora.Prune:output_type -> google.protobuf.Empty
6, // 24: v1.Restora.Forget:output_type -> google.protobuf.Empty
6, // 25: v1.Restora.Restore:output_type -> google.protobuf.Empty
6, // 26: v1.Restora.Unlock:output_type -> google.protobuf.Empty
6, // 27: v1.Restora.Cancel:output_type -> google.protobuf.Empty
14, // 28: v1.Restora.PathAutocomplete:output_type -> types.StringList
15, // [15:29] is the sub-list for method output_type
1, // [1:15] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
+85
View File
@@ -408,6 +408,40 @@ func local_request_Restora_Unlock_0(ctx context.Context, marshaler runtime.Marsh
}
func request_Restora_Cancel_0(ctx context.Context, marshaler runtime.Marshaler, client RestoraClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq types.Int64Value
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Cancel(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Restora_Cancel_0(ctx context.Context, marshaler runtime.Marshaler, server RestoraServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq types.Int64Value
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Cancel(ctx, &protoReq)
return msg, metadata, err
}
func request_Restora_PathAutocomplete_0(ctx context.Context, marshaler runtime.Marshaler, client RestoraClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq types.StringValue
var metadata runtime.ServerMetadata
@@ -730,6 +764,31 @@ func RegisterRestoraHandlerServer(ctx context.Context, mux *runtime.ServeMux, se
})
mux.Handle("POST", pattern_Restora_Cancel_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/v1.Restora/Cancel", runtime.WithHTTPPathPattern("/v1/cmd/cancel"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Restora_Cancel_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Restora_Cancel_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Restora_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -1060,6 +1119,28 @@ func RegisterRestoraHandlerClient(ctx context.Context, mux *runtime.ServeMux, cl
})
mux.Handle("POST", pattern_Restora_Cancel_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/v1.Restora/Cancel", runtime.WithHTTPPathPattern("/v1/cmd/cancel"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Restora_Cancel_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Restora_Cancel_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Restora_PathAutocomplete_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@@ -1110,6 +1191,8 @@ var (
pattern_Restora_Unlock_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "unlock"}, ""))
pattern_Restora_Cancel_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "cmd", "cancel"}, ""))
pattern_Restora_PathAutocomplete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "autocomplete", "path"}, ""))
)
@@ -1138,5 +1221,7 @@ var (
forward_Restora_Unlock_0 = runtime.ForwardResponseMessage
forward_Restora_Cancel_0 = runtime.ForwardResponseMessage
forward_Restora_PathAutocomplete_0 = runtime.ForwardResponseMessage
)
+39
View File
@@ -33,6 +33,7 @@ const (
Restora_Forget_FullMethodName = "/v1.Restora/Forget"
Restora_Restore_FullMethodName = "/v1.Restora/Restore"
Restora_Unlock_FullMethodName = "/v1.Restora/Unlock"
Restora_Cancel_FullMethodName = "/v1.Restora/Cancel"
Restora_PathAutocomplete_FullMethodName = "/v1.Restora/PathAutocomplete"
)
@@ -57,6 +58,8 @@ type RestoraClient interface {
Restore(ctx context.Context, in *RestoreSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Unlock synchronously attempts to unlock the repo. Will block if other operations are in progress.
Unlock(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error)
}
@@ -200,6 +203,15 @@ func (c *restoraClient) Unlock(ctx context.Context, in *types.StringValue, opts
return out, nil
}
func (c *restoraClient) Cancel(ctx context.Context, in *types.Int64Value, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Restora_Cancel_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *restoraClient) PathAutocomplete(ctx context.Context, in *types.StringValue, opts ...grpc.CallOption) (*types.StringList, error) {
out := new(types.StringList)
err := c.cc.Invoke(ctx, Restora_PathAutocomplete_FullMethodName, in, out, opts...)
@@ -230,6 +242,8 @@ type RestoraServer interface {
Restore(context.Context, *RestoreSnapshotRequest) (*emptypb.Empty, error)
// Unlock synchronously attempts to unlock the repo. Will block if other operations are in progress.
Unlock(context.Context, *types.StringValue) (*emptypb.Empty, error)
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error)
// PathAutocomplete provides path autocompletion options for a given filesystem path.
PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error)
mustEmbedUnimplementedRestoraServer()
@@ -275,6 +289,9 @@ func (UnimplementedRestoraServer) Restore(context.Context, *RestoreSnapshotReque
func (UnimplementedRestoraServer) Unlock(context.Context, *types.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Unlock not implemented")
}
func (UnimplementedRestoraServer) Cancel(context.Context, *types.Int64Value) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Cancel not implemented")
}
func (UnimplementedRestoraServer) PathAutocomplete(context.Context, *types.StringValue) (*types.StringList, error) {
return nil, status.Errorf(codes.Unimplemented, "method PathAutocomplete not implemented")
}
@@ -510,6 +527,24 @@ func _Restora_Unlock_Handler(srv interface{}, ctx context.Context, dec func(inte
return interceptor(ctx, in, info, handler)
}
func _Restora_Cancel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(types.Int64Value)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RestoraServer).Cancel(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Restora_Cancel_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RestoraServer).Cancel(ctx, req.(*types.Int64Value))
}
return interceptor(ctx, in, info, handler)
}
func _Restora_PathAutocomplete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(types.StringValue)
if err := dec(in); err != nil {
@@ -579,6 +614,10 @@ var Restora_ServiceDesc = grpc.ServiceDesc{
MethodName: "Unlock",
Handler: _Restora_Unlock_Handler,
},
{
MethodName: "Cancel",
Handler: _Restora_Cancel_Handler,
},
{
MethodName: "PathAutocomplete",
Handler: _Restora_PathAutocomplete_Handler,
+8
View File
@@ -314,6 +314,14 @@ func (s *Server) Unlock(ctx context.Context, req *types.StringValue) (*emptypb.E
return &emptypb.Empty{}, nil
}
func (s *Server) Cancel(ctx context.Context, req *types.Int64Value) (*emptypb.Empty, error) {
if err := s.orchestrator.CancelOperation(req.Value, v1.OperationStatus_STATUS_USER_CANCELLED); err != nil {
return nil, fmt.Errorf("failed to cancel operation %d: %w", req.Value, err)
}
return &emptypb.Empty{}, nil
}
func (s *Server) PathAutocomplete(ctx context.Context, path *types.StringValue) (*types.StringList, error) {
ents, err := os.ReadDir(path.Value)
if errors.Is(err, os.ErrNotExist) {
+57 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"slices"
"sync"
"sync/atomic"
"time"
v1 "github.com/garethgeorge/restora/gen/go/v1"
@@ -19,6 +20,7 @@ import (
var ErrRepoNotFound = errors.New("repo not found")
var ErrRepoInitializationFailed = errors.New("repo initialization failed")
var ErrPlanNotFound = errors.New("plan not found")
var ErrTaskNotFound = errors.New("task not found")
const (
TaskPriorityDefault = iota
@@ -38,6 +40,8 @@ type Orchestrator struct {
// now for the purpose of testing; used by Run() to get the current time.
now func() time.Time
runningTask atomic.Pointer[taskExecutionInfo]
}
func NewOrchestrator(resticBin string, cfg *v1.Config, oplog *oplog.OpLog) (*Orchestrator, error) {
@@ -163,6 +167,41 @@ func (o *Orchestrator) GetPlan(planId string) (*v1.Plan, error) {
return nil, ErrPlanNotFound
}
func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationStatus) error {
o.mu.Lock()
defer o.mu.Unlock()
// note: if the task is running the requested status will not be set.
if running := o.runningTask.Load(); running != nil && running.operationId == operationId {
running.cancel()
}
tasks := o.taskQueue.Reset()
remaining := make([]scheduledTask, 0, len(tasks))
for _, t := range tasks {
if t.task.OperationId() == operationId {
if err := t.task.Cancel(status); err != nil {
return fmt.Errorf("cancel task %q: %w", t.task.Name(), err)
}
// check if the task has a next after it's current 'runAt' time, if it does then we will schedule the next run.
if nextTime := t.task.Next(t.runAt); nextTime != nil {
remaining = append(remaining, scheduledTask{
task: t.task,
runAt: *nextTime,
})
}
} else {
remaining = append(remaining, *t)
}
}
o.taskQueue.Push(remaining...)
return nil
}
// Run is the main orchestration loop. Cancel the context to stop the loop.
func (o *Orchestrator) Run(mainCtx context.Context) {
zap.L().Info("starting orchestrator loop")
@@ -179,12 +218,24 @@ func (o *Orchestrator) Run(mainCtx context.Context) {
}
zap.L().Info("running task", zap.String("task", t.task.Name()))
if err := t.task.Run(mainCtx); err != nil {
taskCtx, cancel := context.WithCancel(mainCtx)
if swapped := o.runningTask.CompareAndSwap(nil, &taskExecutionInfo{
operationId: t.task.OperationId(),
cancel: cancel,
}); !swapped {
zap.L().Fatal("failed to start task, another task is already running. Was Run() called twice?")
}
if err := t.task.Run(taskCtx); err != nil {
zap.L().Error("task failed", zap.String("task", t.task.Name()), zap.Error(err))
} else {
zap.L().Info("task finished", zap.String("task", t.task.Name()))
}
o.runningTask.Store(nil)
if nextTime := t.task.Next(o.curTime()); nextTime != nil {
o.taskQueue.Push(scheduledTask{
task: t.task,
@@ -268,3 +319,8 @@ func (rp *resticRepoPool) GetRepo(repoId string) (repo *RepoOrchestrator, err er
rp.repos[repoId] = repo
return repo, nil
}
type taskExecutionInfo struct {
operationId int64
cancel func()
}
+7 -5
View File
@@ -24,15 +24,17 @@ func (t *taskQueue) curTime() time.Time {
return time.Now()
}
func (t *taskQueue) Push(task scheduledTask) {
func (t *taskQueue) Push(tasks ...scheduledTask) {
t.mu.Lock()
defer t.mu.Unlock()
if task.task == nil {
panic("task cannot be nil")
for _, task := range tasks {
if task.task == nil {
panic("task cannot be nil")
}
heap.Push(&t.heap, &task)
}
heap.Push(&t.heap, &task)
if t.notify != nil {
t.notify <- struct{}{}
}
@@ -57,12 +59,12 @@ func (t *taskQueue) Dequeue(ctx context.Context) *scheduledTask {
t.dequeueMu.Lock()
defer t.dequeueMu.Unlock()
t.mu.Lock()
t.notify = make(chan struct{}, 1)
defer func() {
t.notify = nil
}()
t.mu.Lock()
for {
first, ok := t.heap.Peek().(*scheduledTask)
if !ok { // no tasks in heap.
+27 -32
View File
@@ -16,69 +16,64 @@ type Task interface {
Name() string // huamn readable name for this task.
Next(now time.Time) *time.Time // when this task would like to be run.
Run(ctx context.Context) error // run the task.
Cancel(withStatus v1.OperationStatus) error // cancel the task's execution with the given status (either STATUS_USER_CANCELLED or STATUS_SYSTEM_CANCELLED).
Cancel(withStatus v1.OperationStatus) error // informat the task that it's scheduled execution will be skipped (either STATUS_USER_CANCELLED or STATUS_SYSTEM_CANCELLED).
OperationId() int64 // the id of the operation associated with this task (if any).
}
type TaskWithOperation struct {
orch *Orchestrator
op atomic.Pointer[v1.Operation]
cancelled chan struct{}
orch *Orchestrator
op *v1.Operation
running atomic.Bool
}
func (t *TaskWithOperation) OperationId() int64 {
return t.op.Load().GetId()
if t.op == nil {
return 0
}
return t.op.Id
}
func (t *TaskWithOperation) setOperation(op *v1.Operation) error {
if t.op.Load() != nil {
if t.op != nil {
return errors.New("task already has an operation")
}
if err := t.orch.OpLog.Add(op); err != nil {
return fmt.Errorf("task failed to add operation to oplog: %v", err)
}
t.op.Store(op)
t.cancelled = make(chan struct{}, 1)
t.op = op
return nil
}
func (t *TaskWithOperation) runWithOpAndContext(ctx context.Context, do func(ctx context.Context, op *v1.Operation) error) error {
op := t.op.Load()
if op == nil {
if t.op == nil {
return errors.New("task has no operation, a call to setOperation first is required.")
}
go func() {
t.op.Store(nil)
}()
if t.running.Load() {
return errors.New("task is already running")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
t.running.Store(true)
defer t.running.Store(false)
go func() {
select {
case <-ctx.Done():
case <-t.cancelled:
cancel()
}
}()
return WithOperation(t.orch.OpLog, op, func() error {
return do(ctx, op)
return WithOperation(t.orch.OpLog, t.op, func() error {
return do(ctx, t.op)
})
}
// Cancel marks a task as cancelled. Note that, unintuitively, it is actually an error to call cancel on a running task.
func (t *TaskWithOperation) Cancel(withStatus v1.OperationStatus) error {
close(t.cancelled)
op := t.op.Load()
if op == nil {
if t.running.Load() {
return errors.New("cannot cancel a running task") // should never happen.
}
if t.op == nil {
return nil
}
op.Status = withStatus
op.UnixTimeEndMs = curTimeMillis()
if err := t.orch.OpLog.Update(op); err != nil {
return fmt.Errorf("failed to update operation %v in oplog: %w", op.Id, err)
t.op.Status = withStatus
t.op.UnixTimeEndMs = curTimeMillis()
if err := t.orch.OpLog.Update(t.op); err != nil {
return fmt.Errorf("failed to update operation %v in oplog: %w", t.op.Id, err)
}
t.op = nil
return nil
}
+3 -1
View File
@@ -16,7 +16,7 @@ type CmdError struct {
func (e *CmdError) Error() string {
m := fmt.Sprintf("command %q failed: %s", e.Command, e.Err.Error())
if e.Output != "" {
m += "\nDetails: \n" + e.Output
m += "\nProcess STDOUT: \n" + e.Output
}
return m
}
@@ -35,6 +35,7 @@ func newCmdError(cmd *exec.Cmd, output string, err error) *CmdError {
cerr := &CmdError{
Command: cmd.String(),
Err: err,
Output: output,
}
if len(output) >= outputBufferLimit {
@@ -48,5 +49,6 @@ func newCmdErrorPreformatted(cmd *exec.Cmd, output string, err error) *CmdError
return &CmdError{
Command: cmd.String(),
Err: err,
Output: output,
}
}
+5 -1
View File
@@ -10,4 +10,8 @@ message StringValue {
message StringList {
repeated string values = 1;
}
}
message Int64Value {
int64 value = 1;
}
+8
View File
@@ -99,6 +99,14 @@ service Restora {
};
}
// Cancel attempts to cancel a task with the given operation ID. Not guaranteed to succeed.
rpc Cancel(types.Int64Value) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/cmd/cancel"
body: "*"
};
}
// PathAutocomplete provides path autocompletion options for a given filesystem path.
rpc PathAutocomplete (types.StringValue) returns (types.StringList) {
option (google.api.http) = {
+4
View File
@@ -9,4 +9,8 @@ export type StringValue = {
export type StringList = {
values?: string[]
}
export type Int64Value = {
value?: string
}
+3
View File
@@ -92,6 +92,9 @@ export class Restora {
static Unlock(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise<GoogleProtobufEmpty.Empty> {
return fm.fetchReq<TypesValue.StringValue, GoogleProtobufEmpty.Empty>(`/v1/cmd/unlock`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
static Cancel(req: TypesValue.Int64Value, initReq?: fm.InitReq): Promise<GoogleProtobufEmpty.Empty> {
return fm.fetchReq<TypesValue.Int64Value, GoogleProtobufEmpty.Empty>(`/v1/cmd/cancel`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
static PathAutocomplete(req: TypesValue.StringValue, initReq?: fm.InitReq): Promise<TypesValue.StringList> {
return fm.fetchReq<TypesValue.StringValue, TypesValue.StringList>(`/v1/autocomplete/path`, {...initReq, method: "POST", body: JSON.stringify(req, fm.replacer)})
}
+19 -5
View File
@@ -5,6 +5,7 @@ import {
OperationStatus,
} from "../../gen/ts/v1/operations.pb";
import {
Button,
Card,
Col,
Collapse,
@@ -43,8 +44,9 @@ import {
normalizeSnapshotId,
} from "../lib/formatting";
import _ from "lodash";
import { GetOperationsRequest } from "../../gen/ts/v1/service.pb";
import { GetOperationsRequest, Restora } from "../../gen/ts/v1/service.pb";
import { useAlertApi } from "./Alerts";
import { MessageInstance } from "antd/es/message/interface";
export const OperationList = ({
req,
@@ -121,13 +123,13 @@ export const OperationList = ({
renderItem={(backup) => {
const ops = backup.operations;
if (ops.length === 1) {
return <OperationRow key={ops[0].id!} operation={toEop(ops[0])} />;
return <OperationRow alertApi={alertApi!} key={ops[0].id!} operation={toEop(ops[0])} />;
}
return (
<Card size="small" style={{ margin: "5px" }}>
{ops.map((op) => (
<OperationRow key={op.id!} operation={toEop(op)} />
<OperationRow alertApi={alertApi!} key={op.id!} operation={toEop(op)} />
))}
</Card>
);
@@ -143,7 +145,8 @@ export const OperationList = ({
export const OperationRow = ({
operation,
}: React.PropsWithoutRef<{ operation: EOperation }>) => {
alertApi,
}: React.PropsWithoutRef<{ operation: EOperation, alertApi?: MessageInstance }>) => {
const details = detailsForOperation(operation);
const displayType = getTypeForDisplay(operation);
let avatar: React.ReactNode;
@@ -176,13 +179,24 @@ export const OperationRow = ({
}
const opName = displayTypeToString(getTypeForDisplay(operation));
const title = (
let title = (
<>
{formatTime(operation.unixTimeStartMs!)} - {opName}{" "}
<span className="restora operation-details">{details.displayState}</span>
</>
);
if (operation.status === OperationStatus.STATUS_PENDING || operation.status == OperationStatus.STATUS_INPROGRESS) {
title = <>
{title}
<Button type="link" size="small" onClick={() => {
Restora.Cancel({ value: operation.id! }, { pathPrefix: "/api" }).then(() => {
alertApi?.success("Requested to cancel operation");
})
}}>[Cancel Operation]</Button>
</>
}
let body: React.ReactNode | undefined;
if (
+2 -1
View File
@@ -93,12 +93,13 @@ export const OperationTree = ({
let oplist: React.ReactNode | null = null;
if (selectedBackupId) {
const backup = backups.find((b) => b.id === selectedBackupId);
if (!backup) {
oplist = <Empty description="Backup not found." />;
} else {
oplist = (
<>
<h3>Backup at {formatTime(backup.displayTime)}</h3>
<h3>Backup on {formatTime(backup.displayTime)}</h3>
<OperationList key={backup.id} useBackups={[backup]} />
</>
);