From 62154da17c1c77d0ea69cef652c7fd0199d31e20 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 22 Apr 2022 16:30:20 +0300 Subject: [PATCH] [#1324] services/tree: Implement Object Tree Service Object Tree Service allows changing trees assotiated with the container in runtime. Signed-off-by: Evgenii Stratonikov --- cmd/neofs-node/config/tree/config.go | 39 +++ cmd/neofs-node/config/tree/config_test.go | 30 +++ cmd/neofs-node/main.go | 1 + cmd/neofs-node/tree.go | 41 +++ config/example/node.env | 3 + config/example/node.json | 5 + config/example/node.yaml | 4 + go.mod | 8 + go.sum | 6 + pkg/services/tree/options.go | 59 +++++ pkg/services/tree/replicator.go | 145 +++++++++++ pkg/services/tree/service.go | 294 ++++++++++++++++++++++ pkg/services/tree/service.pb.go | Bin 0 -> 84811 bytes pkg/services/tree/service.proto | 194 ++++++++++++++ pkg/services/tree/service_grpc.pb.go | Bin 0 -> 12685 bytes pkg/services/tree/signature.go | 44 ++++ pkg/services/tree/types.pb.go | Bin 0 -> 9666 bytes pkg/services/tree/types.proto | 27 ++ 18 files changed, 900 insertions(+) create mode 100644 cmd/neofs-node/config/tree/config.go create mode 100644 cmd/neofs-node/config/tree/config_test.go create mode 100644 cmd/neofs-node/tree.go create mode 100644 pkg/services/tree/options.go create mode 100644 pkg/services/tree/replicator.go create mode 100644 pkg/services/tree/service.go create mode 100644 pkg/services/tree/service.pb.go create mode 100644 pkg/services/tree/service.proto create mode 100644 pkg/services/tree/service_grpc.pb.go create mode 100644 pkg/services/tree/signature.go create mode 100644 pkg/services/tree/types.pb.go create mode 100644 pkg/services/tree/types.proto diff --git a/cmd/neofs-node/config/tree/config.go b/cmd/neofs-node/config/tree/config.go new file mode 100644 index 000000000..8fdc11011 --- /dev/null +++ b/cmd/neofs-node/config/tree/config.go @@ -0,0 +1,39 @@ +package treeconfig + +import ( + "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config" +) + +// GRPCConfig is a wrapper over "grpc" config section which provides access +// to gRPC configuration of control service. +type GRPCConfig struct { + cfg *config.Config +} + +const ( + subsection = "tree" + grpcSubsection = "grpc" + + // GRPCEndpointDefault is a default endpoint of gRPC Control service. + GRPCEndpointDefault = "" +) + +// GRPC returns structure that provides access to "grpc" subsection of +// "tree" section. +func GRPC(c *config.Config) GRPCConfig { + return GRPCConfig{ + c.Sub(subsection).Sub(grpcSubsection), + } +} + +// Endpoint returns value of "endpoint" config parameter. +// +// Returns GRPCEndpointDefault if value is not a non-empty string. +func (g GRPCConfig) Endpoint() string { + v := config.String(g.cfg, "endpoint") + if v != "" { + return v + } + + return GRPCEndpointDefault +} diff --git a/cmd/neofs-node/config/tree/config_test.go b/cmd/neofs-node/config/tree/config_test.go new file mode 100644 index 000000000..9ac92fe6d --- /dev/null +++ b/cmd/neofs-node/config/tree/config_test.go @@ -0,0 +1,30 @@ +package treeconfig_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config" + configtest "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/test" + treeconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/tree" + "github.com/stretchr/testify/require" +) + +func TestControlSection(t *testing.T) { + t.Run("defaults", func(t *testing.T) { + empty := configtest.EmptyConfig() + + require.Equal(t, treeconfig.GRPCEndpointDefault, treeconfig.GRPC(empty).Endpoint()) + }) + + const path = "../../../../config/example/node" + + var fileConfigTest = func(c *config.Config) { + require.Equal(t, "127.0.0.1:8091", treeconfig.GRPC(c).Endpoint()) + } + + configtest.ForEachFileType(path, fileConfigTest) + + t.Run("ENV", func(t *testing.T) { + configtest.ForEnvFileType(path, fileConfigTest) + }) +} diff --git a/cmd/neofs-node/main.go b/cmd/neofs-node/main.go index 807dce4c3..67aca2812 100644 --- a/cmd/neofs-node/main.go +++ b/cmd/neofs-node/main.go @@ -81,6 +81,7 @@ func initApp(c *cfg) { initAndLog(c, "pprof", initProfiler) initAndLog(c, "prometheus", initMetrics) initAndLog(c, "control", initControlService) + initAndLog(c, "tree", initTreeService) initAndLog(c, "storage engine", func(c *cfg) { fatalOnErr(c.cfgObject.cfgLocalStorage.localStorage.Open()) diff --git a/cmd/neofs-node/tree.go b/cmd/neofs-node/tree.go new file mode 100644 index 000000000..37dd331e6 --- /dev/null +++ b/cmd/neofs-node/tree.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "net" + + treeconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/tree" + "github.com/nspcc-dev/neofs-node/pkg/services/tree" + "google.golang.org/grpc" +) + +func initTreeService(c *cfg) { + endpoint := treeconfig.GRPC(c.appCfg).Endpoint() + if endpoint == treeconfig.GRPCEndpointDefault { + return + } + + treeSvc := tree.New( + tree.WithContainerSource(c.cfgObject.cnrSource), + tree.WithNetmapSource(c.netMapSource), + tree.WithPrivateKey(&c.key.PrivateKey), + tree.WithLogger(c.log), + tree.WithStorage(c.cfgObject.cfgLocalStorage.localStorage)) + + treeServer := grpc.NewServer() + + c.onShutdown(func() { + stopGRPC("NeoFS Tree Service API", treeServer, c.log) + treeSvc.Shutdown() + }) + + lis, err := net.Listen("tcp", endpoint) + fatalOnErr(err) + + tree.RegisterTreeServiceServer(treeServer, treeSvc) + + c.workers = append(c.workers, newWorkerFromFunc(func(ctx context.Context) { + treeSvc.Start(ctx) + fatalOnErr(treeServer.Serve(lis)) + })) +} diff --git a/config/example/node.env b/config/example/node.env index 3c674ee71..1c31501f0 100644 --- a/config/example/node.env +++ b/config/example/node.env @@ -46,6 +46,9 @@ NEOFS_GRPC_1_TLS_ENABLED=false NEOFS_CONTROL_AUTHORIZED_KEYS="035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6" NEOFS_CONTROL_GRPC_ENDPOINT=localhost:8090 +# Tree service section +NEOFS_TREE_GRPC_ENDPOINT=127.0.0.1:8091 + # Contracts section NEOFS_CONTRACTS_BALANCE=5263abba1abedbf79bb57f3e40b50b4425d2d6cd NEOFS_CONTRACTS_CONTAINER=5d084790d7aa36cea7b53fe897380dab11d2cd3c diff --git a/config/example/node.json b/config/example/node.json index 569aff46f..b3ae78508 100644 --- a/config/example/node.json +++ b/config/example/node.json @@ -84,6 +84,11 @@ "endpoint": "localhost:8090" } }, + "tree": { + "grpc": { + "endpoint": "127.0.0.1:8091" + } + }, "contracts": { "balance": "5263abba1abedbf79bb57f3e40b50b4425d2d6cd", "container": "5d084790d7aa36cea7b53fe897380dab11d2cd3c", diff --git a/config/example/node.yaml b/config/example/node.yaml index b3eec6380..3465eb071 100644 --- a/config/example/node.yaml +++ b/config/example/node.yaml @@ -66,6 +66,10 @@ control: grpc: endpoint: localhost:8090 # endpoint that is listened by the Control Service +tree: + grpc: + endpoint: 127.0.0.1:8091 # endpoint that is listened by the Tree Service + contracts: # side chain NEOFS contract script hashes; optional, override values retrieved from NNS contract balance: 5263abba1abedbf79bb57f3e40b50b4425d2d6cd container: 5d084790d7aa36cea7b53fe897380dab11d2cd3c diff --git a/go.mod b/go.mod index a6ccbecc1..46a96c06f 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require google.golang.org/api v0.44.0 + require ( + cloud.google.com/go v0.81.0 // indirect github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd v0.22.0-beta // indirect @@ -47,8 +50,10 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect + github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -87,12 +92,15 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect github.com/twmb/murmur3 v1.1.5 // indirect github.com/urfave/cli v1.22.5 // indirect + go.opencensus.io v0.23.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 1c82bb69f..c0f0a43cc 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,7 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -161,6 +162,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -559,6 +561,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -687,6 +690,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -871,6 +875,7 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -878,6 +883,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/pkg/services/tree/options.go b/pkg/services/tree/options.go new file mode 100644 index 000000000..2cf2bdf45 --- /dev/null +++ b/pkg/services/tree/options.go @@ -0,0 +1,59 @@ +package tree + +import ( + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-node/pkg/core/container" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" + "go.uber.org/zap" +) + +type cfg struct { + log *zap.Logger + key *ecdsa.PrivateKey + nmSource netmap.Source + cnrSource container.Source + forest pilorama.Forest +} + +// Option represents configuration option for a tree service. +type Option func(*cfg) + +// WithContainerSource sets a container source for a tree service. +// This option is required. +func WithContainerSource(src container.Source) Option { + return func(c *cfg) { + c.cnrSource = src + } +} + +// WithNetmapSource sets a netmap source for a tree service. +// This option is required. +func WithNetmapSource(src netmap.Source) Option { + return func(c *cfg) { + c.nmSource = src + } +} + +// WithPrivateKey sets a netmap source for a tree service. +// This option is required. +func WithPrivateKey(key *ecdsa.PrivateKey) Option { + return func(c *cfg) { + c.key = key + } +} + +// WithLogger sets logger for a tree service. +func WithLogger(log *zap.Logger) Option { + return func(c *cfg) { + c.log = log + } +} + +// WithStorage sets tree storage for a service. +func WithStorage(s pilorama.Forest) Option { + return func(c *cfg) { + c.forest = s + } +} diff --git a/pkg/services/tree/replicator.go b/pkg/services/tree/replicator.go new file mode 100644 index 000000000..87cced5b4 --- /dev/null +++ b/pkg/services/tree/replicator.go @@ -0,0 +1,145 @@ +package tree + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "time" + + clientcore "github.com/nspcc-dev/neofs-node/pkg/core/client" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" + "github.com/nspcc-dev/neofs-node/pkg/network" + "github.com/nspcc-dev/neofs-node/pkg/services/object_manager/placement" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" + "go.uber.org/zap" + "google.golang.org/api/option" + "google.golang.org/api/transport/grpc" +) + +type movePair struct { + cid cidSDK.ID + treeID string + op *pilorama.LogMove +} + +const ( + defaultReplicatorCapacity = 64 + defaultReplicatorTimeout = time.Second * 2 +) + +func (s *Service) replicateLoop(ctx context.Context) { + for { + select { + case <-s.closeCh: + case <-ctx.Done(): + return + case op := <-s.replicateCh: + ctx, cancel := context.WithTimeout(ctx, defaultReplicatorTimeout) + err := s.replicate(ctx, op) + cancel() + + if err != nil { + s.log.Error("error during replication", + zap.String("err", err.Error()), + zap.Stringer("cid", op.cid), + zap.String("treeID", op.treeID)) + } + } + } +} + +func (s *Service) replicate(ctx context.Context, op movePair) error { + req := newApplyRequest(&op) + // TODO(@fyrchik): #1328 access control + //err := signature.SignDataWithHandler(s.key, req, func(key, sign []byte) { + // req.Signature = &Signature{ + // Key: key, + // Sign: sign, + // } + //}) + //if err != nil { + // return fmt.Errorf("can't sign data: %w", err) + //} + + nodes, err := s.getContainerNodes(op.cid) + if err != nil { + return fmt.Errorf("can't get container nodes: %w", err) + } + + var node clientcore.NodeInfo + for _, n := range nodes { + var lastErr error + + n.IterateNetworkEndpoints(func(addr string) bool { + cc, err := grpc.Dial(ctx, option.WithEndpoint(addr)) + if err != nil { + lastErr = err + return false + } + + // TODO cache clients + c := NewTreeServiceClient(cc) + + _, lastErr = c.Apply(ctx, req) + return lastErr == nil + }) + + if lastErr != nil { + s.log.Warn("failed to sent update to the node", + zap.String("last_error", lastErr.Error()), + zap.String("address", network.StringifyGroup(node.AddressGroup())), + zap.String("key", base64.StdEncoding.EncodeToString(node.PublicKey()))) + } + } + return nil +} + +func (s *Service) pushToQueue(cid cidSDK.ID, treeID string, op *pilorama.LogMove) { + s.replicateCh <- movePair{ + cid: cid, + treeID: treeID, + op: op, + } +} + +func (s *Service) getContainerNodes(cid cidSDK.ID) ([]netmapSDK.NodeInfo, error) { + nm, err := s.nmSource.GetNetMap(0) + if err != nil { + return nil, fmt.Errorf("can't get netmap: %w", err) + } + + cnr, err := s.cnrSource.Get(cid) + if err != nil { + return nil, fmt.Errorf("can't get container: %w", err) + } + + policy := cnr.Value.PlacementPolicy() + rawCID := make([]byte, sha256.Size) + cid.Encode(rawCID) + + nodes, err := nm.ContainerNodes(policy, rawCID) + if err != nil { + return nil, err + } + + return placement.FlattenNodes(nodes), nil +} + +func newApplyRequest(op *movePair) *ApplyRequest { + rawCID := make([]byte, sha256.Size) + op.cid.Encode(rawCID) + + return &ApplyRequest{ + Body: &ApplyRequest_Body{ + ContainerId: rawCID, + TreeId: op.treeID, + Operation: &LogMove{ + ParentId: op.op.Parent, + Meta: op.op.Meta.Bytes(), + ChildId: op.op.Child, + }, + }, + } +} diff --git a/pkg/services/tree/service.go b/pkg/services/tree/service.go new file mode 100644 index 000000000..00de22b8a --- /dev/null +++ b/pkg/services/tree/service.go @@ -0,0 +1,294 @@ +package tree + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + "go.uber.org/zap" +) + +// Service represents tree-service capable of working with multiple +// instances of CRDT trees. +type Service struct { + cfg + + replicateCh chan movePair + closeCh chan struct{} +} + +var _ TreeServiceServer = (*Service)(nil) + +// New creates new tree service instance. +func New(opts ...Option) *Service { + var s Service + for i := range opts { + opts[i](&s.cfg) + } + + if s.log == nil { + s.log = zap.NewNop() + } + + s.closeCh = make(chan struct{}) + s.replicateCh = make(chan movePair, defaultReplicatorCapacity) + + return &s +} + +// Start starts the service. +func (s *Service) Start(ctx context.Context) { + go s.replicateLoop(ctx) +} + +// Shutdown shutdowns the service. +func (s *Service) Shutdown() { + close(s.closeCh) +} + +func (s *Service) Add(_ context.Context, req *AddRequest) (*AddResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + log, err := s.forest.TreeMove(cid, b.GetTreeId(), &pilorama.Move{ + Parent: b.GetParentId(), + Child: pilorama.RootID, + Meta: pilorama.Meta{Items: constructMeta(b.GetMeta())}, + }) + if err != nil { + return nil, err + } + + s.pushToQueue(cid, b.GetTreeId(), log) + return &AddResponse{ + Body: &AddResponse_Body{ + NodeId: log.Child, + }, + }, nil +} + +func (s *Service) AddByPath(_ context.Context, req *AddByPathRequest) (*AddByPathResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + meta := constructMeta(b.GetMeta()) + + attr := b.GetPathAttribute() + if len(attr) == 0 { + attr = pilorama.AttributeFilename + } + + logs, err := s.forest.TreeAddByPath(cid, b.GetTreeId(), attr, b.GetPath(), meta) + if err != nil { + return nil, err + } + + for i := range logs { + s.pushToQueue(cid, b.GetTreeId(), &logs[i]) + } + + nodes := make([]uint64, len(logs)) + nodes[0] = logs[len(logs)-1].Child + for i, l := range logs[:len(logs)-1] { + nodes[i+1] = l.Child + } + + return &AddByPathResponse{ + Body: &AddByPathResponse_Body{ + Nodes: nodes, + }, + }, nil +} + +func (s *Service) Remove(_ context.Context, req *RemoveRequest) (*RemoveResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + if b.GetNodeId() == pilorama.RootID { + return nil, fmt.Errorf("node with ID %d is root and can't be removed", b.GetNodeId()) + } + + log, err := s.forest.TreeMove(cid, b.GetTreeId(), &pilorama.Move{ + Parent: pilorama.TrashID, + Child: b.GetNodeId(), + }) + if err != nil { + return nil, err + } + + s.pushToQueue(cid, b.GetTreeId(), log) + return new(RemoveResponse), nil +} + +// Move applies client operation to the specified tree and pushes in queue +// for replication on other nodes. +func (s *Service) Move(_ context.Context, req *MoveRequest) (*MoveResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + if b.GetNodeId() == pilorama.RootID { + return nil, fmt.Errorf("node with ID %d is root and can't be moved", b.GetNodeId()) + } + + log, err := s.forest.TreeMove(cid, b.GetTreeId(), &pilorama.Move{ + Parent: b.GetParentId(), + Child: b.GetNodeId(), + Meta: pilorama.Meta{Items: constructMeta(b.GetMeta())}, + }) + if err != nil { + return nil, err + } + + s.pushToQueue(cid, b.GetTreeId(), log) + return new(MoveResponse), nil +} + +func (s *Service) GetNodeByPath(_ context.Context, req *GetNodeByPathRequest) (*GetNodeByPathResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + attr := b.GetPathAttribute() + if len(attr) == 0 { + attr = pilorama.AttributeFilename + } + + nodes, err := s.forest.TreeGetByPath(cid, b.GetTreeId(), attr, b.GetPath(), b.GetLatestOnly()) + if err != nil { + return nil, err + } + + info := make([]*GetNodeByPathResponse_Info, 0, len(nodes)) + for _, node := range nodes { + m, err := s.forest.TreeGetMeta(cid, b.GetTreeId(), node) + if err != nil { + return nil, err + } + + var x GetNodeByPathResponse_Info + x.NodeId = node + x.Timestamp = m.Time + for _, kv := range m.Items { + needAttr := b.AllAttributes + if !needAttr { + for _, attr := range b.GetAttributes() { + if kv.Key == attr { + needAttr = true + break + } + } + } + + if needAttr { + x.Meta = append(x.Meta, &KeyValue{ + Key: kv.Key, + Value: kv.Value, + }) + } + } + info = append(info, &x) + } + + return &GetNodeByPathResponse{ + Body: &GetNodeByPathResponse_Body{ + Nodes: info, + }, + }, nil +} + +func (s *Service) GetSubTree(_ context.Context, req *GetSubTreeRequest) (*GetSubTreeResponse, error) { + return nil, errors.New("GetSubTree is unimplemented") +} + +// Apply locally applies operation from the remote node to the tree. +func (s *Service) Apply(_ context.Context, req *ApplyRequest) (*ApplyResponse, error) { + err := signature.VerifyServiceMessage(req) + if err != nil { + return nil, err + } + + var cid cidSDK.ID + if err := cid.Decode(req.GetBody().GetContainerId()); err != nil { + return nil, err + } + + found := false + key := req.GetSignature().GetKey() + nodes, _ := s.getContainerNodes(cid) + +loop: + for _, n := range nodes { + if bytes.Equal(key, n.PublicKey()) { + found = true + break loop + } + } + if !found { + return nil, errors.New("`Apply` request must be signed by a container node") + } + + op := req.GetBody().GetOperation() + + var meta pilorama.Meta + if err := meta.FromBytes(op.GetMeta()); err != nil { + return nil, fmt.Errorf("can't parse meta-information: %w", err) + } + + return nil, s.forest.TreeApply(cid, req.GetBody().GetTreeId(), &pilorama.Move{ + Parent: op.GetParentId(), + Child: op.GetChildId(), + Meta: meta, + }) +} + +func constructMeta(arr []*KeyValue) []pilorama.KeyValue { + meta := make([]pilorama.KeyValue, len(arr)) + for i, kv := range arr { + meta[i].Key = kv.Key + meta[i].Value = kv.Value + } + return meta +} diff --git a/pkg/services/tree/service.pb.go b/pkg/services/tree/service.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..bf1f26904e17e6e6bbdfdcbcb98729981b5d02ce GIT binary patch literal 84811 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 EpZIMXHvj+t literal 0 HcmV?d00001 diff --git a/pkg/services/tree/types.proto b/pkg/services/tree/types.proto new file mode 100644 index 000000000..420fa4dfa --- /dev/null +++ b/pkg/services/tree/types.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package tree; + +option go_package = "github.com/nspcc-dev/neofs-node/pkg/services/tree"; + +// KeyValue represents key-value pair attached to an object. +message KeyValue { + string key = 1 [json_name = "key"]; + bytes value = 2 [json_name = "value"]; +} + +// LogMove represents log-entry for a single move operation. +message LogMove { + // ID of the parent node. + uint64 parent_id = 2 [json_name = "parentID"]; + // Node meta information, including operation timestamp. + bytes meta = 3 [json_name = "meta"]; + // ID of the node to move. + uint64 child_id = 4 [json_name = "childID"]; +} + +// Signature of a message. +message Signature { + bytes key = 1 [json_name = "key"]; + bytes sign = 2 [json_name = "signature"]; +}