mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-25 18:36:31 +00:00
feat: support task cancellation
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,8 @@ message StringValue {
|
||||
|
||||
message StringList {
|
||||
repeated string values = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Int64Value {
|
||||
int64 value = 1;
|
||||
}
|
||||
|
||||
@@ -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) = {
|
||||
|
||||
@@ -9,4 +9,8 @@ export type StringValue = {
|
||||
|
||||
export type StringList = {
|
||||
values?: string[]
|
||||
}
|
||||
|
||||
export type Int64Value = {
|
||||
value?: string
|
||||
}
|
||||
@@ -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)})
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]} />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user