diff --git a/config/config.yaml b/config/config.yaml index 5a806628..7188176a 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -163,6 +163,17 @@ checks: node-group: bee timeout: 5m type: cashout + chunk-stream: + options: + chunks-per-node: 10 + postage-ttl: 24h + postage-depth: 21 + postage-label: test-label + request-batch-size: 5 + request-timeout: 60s + upload-node-count: 3 + timeout: 5m + type: chunk-stream datadurability: options: ref: diff --git a/config/local.yaml b/config/local.yaml index d389ee2b..d784f4d1 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -234,6 +234,17 @@ checks: seed: 0 timeout: 5m type: act + ci-chunk-stream: + options: + chunks-per-node: 10 + postage-ttl: 24h + postage-depth: 21 + postage-label: test-label + request-batch-size: 5 + request-timeout: 60s + upload-node-count: 3 + timeout: 5m + type: chunk-stream ci-full-connectivity: timeout: 5m type: full-connectivity diff --git a/pkg/bee/api/api.go b/pkg/bee/api/api.go index f943fc33..102db04f 100644 --- a/pkg/bee/api/api.go +++ b/pkg/bee/api/api.go @@ -45,6 +45,7 @@ type Client struct { Act *ActService Bytes *BytesService Chunks *ChunksService + ChunkStream *ChunkStreamService Dirs *DirsService Feed *FeedService Files *FilesService @@ -95,6 +96,7 @@ func newClient(apiURL *url.URL, httpClient *http.Client) (c *Client) { c.Act = (*ActService)(&c.service) c.Bytes = (*BytesService)(&c.service) c.Chunks = (*ChunksService)(&c.service) + c.ChunkStream = (*ChunkStreamService)(&c.service) c.Dirs = (*DirsService)(&c.service) c.Feed = (*FeedService)(&c.service) c.Files = (*FilesService)(&c.service) diff --git a/pkg/bee/api/chunkstream.go b/pkg/bee/api/chunkstream.go new file mode 100644 index 00000000..351999d6 --- /dev/null +++ b/pkg/bee/api/chunkstream.go @@ -0,0 +1,233 @@ +package api + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "slices" + "time" + + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/gorilla/websocket" +) + +// ChunkStreamService represents Bee's chunk stream service, a websocket +// endpoint that multiplexes many chunk uploads or downloads over a single +// connection. +type ChunkStreamService service + +// Delivery status reported by the download stream for each requested address. +const ( + ChunkDeliverySuccess byte = 0x00 + ChunkDeliveryNotFound byte = 0x01 + ChunkDeliveryError byte = 0x02 +) + +const ( + chunkStreamUploadSubprotocol = "swarm-chunk-upload" + chunkStreamDownloadSubprotocol = "swarm-chunk-download" + chunkStreamHandshakeTimeout = 45 * time.Second + + // chunkDownloadOpcode is the command byte that prefixes a download request + // frame: [opcode][32-byte address]... + chunkDownloadOpcode byte = 'D' +) + +func chunkStreamPath() string { + return "/" + apiVersion + "/chunks/stream" +} + +// ChunkDelivery is a single response frame from the download stream. Data is +// only set when Status is ChunkDeliverySuccess. +type ChunkDelivery struct { + Status byte + Address swarm.Address + Data []byte +} + +// ChunkUploadStream uploads chunks over a single websocket connection. It is +// not safe for concurrent use: each Upload writes a chunk and waits for the +// node to acknowledge it. +type ChunkUploadStream struct { + conn *websocket.Conn +} + +// NewUploadStream opens a chunk stream in upload mode. Chunks are stamped with +// the given postage batch and pushed to the network as they arrive. +func (c *ChunkStreamService) NewUploadStream(ctx context.Context, batchID string) (*ChunkUploadStream, error) { + header := http.Header{} + header.Set(postageStampBatchHeader, batchID) + + conn, err := c.client.dialWebSocket(ctx, chunkStreamPath(), chunkStreamUploadSubprotocol, header) + if err != nil { + return nil, err + } + + return &ChunkUploadStream{conn: conn}, nil +} + +// Upload writes one chunk and waits for the node's acknowledgement, which the +// node sends as an empty binary frame. +func (s *ChunkUploadStream) Upload(data []byte, timeout time.Duration) error { + if err := s.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { + return fmt.Errorf("set write deadline: %w", err) + } + if err := s.conn.WriteMessage(websocket.BinaryMessage, data); err != nil { + return fmt.Errorf("write chunk: %w", err) + } + + if err := s.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + return fmt.Errorf("set read deadline: %w", err) + } + mt, msg, err := s.conn.ReadMessage() + if err != nil { + return fmt.Errorf("read acknowledgement: %w", err) + } + if mt != websocket.BinaryMessage { + return fmt.Errorf("acknowledgement: expected a binary frame, got message type %d", mt) + } + if len(msg) != 0 { + return fmt.Errorf("acknowledgement: expected an empty frame, got %d bytes", len(msg)) + } + + return nil +} + +// Close shuts the stream down, giving the node a chance to finish syncing. +func (s *ChunkUploadStream) Close() error { + return closeWebSocket(s.conn) +} + +// ChunkDownloadStream requests chunks over a single websocket connection. +// Requests are pipelined and the node answers out of order, so callers must +// match deliveries by address rather than by request order. +type ChunkDownloadStream struct { + conn *websocket.Conn +} + +// NewDownloadStream opens a chunk stream in download mode. +func (c *ChunkStreamService) NewDownloadStream(ctx context.Context) (*ChunkDownloadStream, error) { + conn, err := c.client.dialWebSocket(ctx, chunkStreamPath(), chunkStreamDownloadSubprotocol, http.Header{}) + if err != nil { + return nil, err + } + + return &ChunkDownloadStream{conn: conn}, nil +} + +// Request asks for the given addresses in a single frame, sent as +// [opcode][32-byte address]... The node answers with exactly one delivery per +// address, in no particular order. +func (s *ChunkDownloadStream) Request(addrs []swarm.Address, timeout time.Duration) error { + if len(addrs) == 0 { + return errors.New("no addresses requested") + } + + frame := make([]byte, 0, 1+len(addrs)*swarm.HashSize) + frame = append(frame, chunkDownloadOpcode) + for _, a := range addrs { + if len(a.Bytes()) != swarm.HashSize { + return fmt.Errorf("address %s is not %d bytes", a, swarm.HashSize) + } + frame = append(frame, a.Bytes()...) + } + + if err := s.conn.SetWriteDeadline(time.Now().Add(timeout)); err != nil { + return fmt.Errorf("set write deadline: %w", err) + } + if err := s.conn.WriteMessage(websocket.BinaryMessage, frame); err != nil { + return fmt.Errorf("request %d addresses: %w", len(addrs), err) + } + + return nil +} + +// Receive reads the next delivery from the stream. +func (s *ChunkDownloadStream) Receive(timeout time.Duration) (ChunkDelivery, error) { + if err := s.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { + return ChunkDelivery{}, fmt.Errorf("set read deadline: %w", err) + } + + mt, msg, err := s.conn.ReadMessage() + if err != nil { + return ChunkDelivery{}, fmt.Errorf("read delivery: %w", err) + } + if mt != websocket.BinaryMessage { + return ChunkDelivery{}, fmt.Errorf("delivery: expected a binary frame, got message type %d", mt) + } + if len(msg) < 1+swarm.HashSize { + return ChunkDelivery{}, fmt.Errorf("delivery: frame of %d bytes is shorter than the %d byte header", len(msg), 1+swarm.HashSize) + } + + d := ChunkDelivery{ + Status: msg[0], + Address: swarm.NewAddress(slices.Clone(msg[1 : 1+swarm.HashSize])), + } + if d.Status == ChunkDeliverySuccess { + d.Data = slices.Clone(msg[1+swarm.HashSize:]) + } + + return d, nil +} + +// Close shuts the stream down. +func (s *ChunkDownloadStream) Close() error { + return closeWebSocket(s.conn) +} + +// dialWebSocket opens a websocket connection to path on the node, negotiating +// the given subprotocol. +func (c *Client) dialWebSocket(ctx context.Context, path, subprotocol string, header http.Header) (*websocket.Conn, error) { + full, err := c.getFullURL(path) + if err != nil { + return nil, err + } + + u, err := url.Parse(full) + if err != nil { + return nil, fmt.Errorf("parse websocket url: %w", err) + } + if u.Scheme == "https" { + u.Scheme = "wss" + } else { + u.Scheme = "ws" + } + + dialer := &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: chunkStreamHandshakeTimeout, + Subprotocols: []string{subprotocol}, + } + + conn, resp, err := dialer.DialContext(ctx, u.String(), header) + if resp != nil { + defer drain(resp.Body) + } + if err != nil { + if resp != nil { + return nil, fmt.Errorf("dial %s: %w (%s)", u, err, resp.Status) + } + return nil, fmt.Errorf("dial %s: %w", u, err) + } + + if got := resp.Header.Get("Sec-WebSocket-Protocol"); got != subprotocol { + defer conn.Close() + return nil, fmt.Errorf("node did not negotiate subprotocol %s, got %q", subprotocol, got) + } + + return conn, nil +} + +func closeWebSocket(conn *websocket.Conn) error { + msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + if err := conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(5*time.Second)); err != nil && + !errors.Is(err, websocket.ErrCloseSent) { + // Best effort: still close the underlying connection below. + _ = conn.Close() + return fmt.Errorf("send close message: %w", err) + } + + return conn.Close() +} diff --git a/pkg/check/chunkstream/chunkstream.go b/pkg/check/chunkstream/chunkstream.go new file mode 100644 index 00000000..a3a9777d --- /dev/null +++ b/pkg/check/chunkstream/chunkstream.go @@ -0,0 +1,270 @@ +package chunkstream + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/rand" + "time" + + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/ethersphere/beekeeper/pkg/bee" + "github.com/ethersphere/beekeeper/pkg/bee/api" + "github.com/ethersphere/beekeeper/pkg/beekeeper" + "github.com/ethersphere/beekeeper/pkg/logging" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "github.com/ethersphere/beekeeper/pkg/random" +) + +// Options represents check options +type Options struct { + ChunksPerNode int // number of chunks to stream per upload node + PostageTTL time.Duration + PostageDepth uint64 + PostageLabel string + RequestBatchSize int // addresses per download request frame + RequestTimeout time.Duration // per websocket read or write + Seed int64 + SkipNotFound bool // skip the not-found assertion + UploadNodeCount int +} + +// NewDefaultOptions returns new default options +func NewDefaultOptions() Options { + return Options{ + ChunksPerNode: 10, + PostageTTL: 24 * time.Hour, + PostageDepth: 16, + PostageLabel: "test-label", + RequestBatchSize: 5, + RequestTimeout: 60 * time.Second, + Seed: random.Int64(), + SkipNotFound: false, + UploadNodeCount: 1, + } +} + +// compile check whether Check implements interface +var _ beekeeper.Action = (*Check)(nil) + +// Check instance +type Check struct { + metrics metrics + logger logging.Logger +} + +// NewCheck returns new check +func NewCheck(logger logging.Logger) beekeeper.Action { + return &Check{ + metrics: newMetrics(), + logger: logger, + } +} + +var errChunkStream = errors.New("chunk stream") + +// Run streams chunks onto the cluster over the /chunks/stream websocket +// endpoint and streams them back from a different node over the same endpoint, +// verifying that every requested address gets exactly one correct delivery. +func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any) error { + o, ok := opts.(Options) + if !ok { + return fmt.Errorf("invalid options type") + } + if o.ChunksPerNode < 1 { + return fmt.Errorf("chunks-per-node must be at least 1, got %d", o.ChunksPerNode) + } + if o.RequestBatchSize < 1 { + return fmt.Errorf("request-batch-size must be at least 1, got %d", o.RequestBatchSize) + } + + rnds := random.PseudoGenerators(o.Seed, o.UploadNodeCount) + + clients, err := cluster.NodesClients(ctx) + if err != nil { + return err + } + + nodes := cluster.FullNodeNames() + if len(nodes) < 2 { + return fmt.Errorf("at least 2 full nodes are needed, got %d", len(nodes)) + } + + for i := range o.UploadNodeCount { + uploadNode := clients[nodes[i]] + downloadNode := clients[nodes[(i+1)%len(nodes)]] // download from the next node + + batchID, err := uploadNode.GetOrCreateMutableBatch(ctx, o.PostageTTL, o.PostageDepth, o.PostageLabel) + if err != nil { + return fmt.Errorf("node %s: create batch: %w", uploadNode.Name(), err) + } + c.logger.Infof("node %s: using batch %s", uploadNode.Name(), batchID) + + chunks, err := c.streamUpload(ctx, uploadNode, batchID, rnds[i], o) + if err != nil { + return fmt.Errorf("node %s: upload stream: %w", uploadNode.Name(), err) + } + + if err := c.streamDownload(ctx, downloadNode, chunks, o); err != nil { + return fmt.Errorf("node %s: download stream: %w", downloadNode.Name(), err) + } + + if !o.SkipNotFound { + if err := c.checkNotFound(ctx, downloadNode, rnds[i], o); err != nil { + return fmt.Errorf("node %s: not-found delivery: %w", downloadNode.Name(), err) + } + } + } + + return nil +} + +// streamUpload uploads chunks over a single websocket connection and returns +// them keyed by address. +func (c *Check) streamUpload(ctx context.Context, node *bee.Client, batchID string, rnd *rand.Rand, o Options) (map[string]bee.Chunk, error) { + stream, err := node.API().ChunkStream.NewUploadStream(ctx, batchID) + if err != nil { + return nil, fmt.Errorf("open: %w", err) + } + defer func() { + if err := stream.Close(); err != nil { + c.logger.Debugf("node %s: closing upload stream: %v", node.Name(), err) + } + }() + + chunks := make(map[string]bee.Chunk, o.ChunksPerNode) + + start := time.Now() + for range o.ChunksPerNode { + chunk, err := bee.NewRandomChunk(rnd, c.logger) + if err != nil { + return nil, fmt.Errorf("create chunk: %w", err) + } + + if err := stream.Upload(chunk.Data(), o.RequestTimeout); err != nil { + c.metrics.UploadErrorCounter.WithLabelValues(node.Name()).Inc() + return nil, fmt.Errorf("chunk %s: %w", chunk.Address(), err) + } + + chunks[chunk.Address().String()] = chunk + c.metrics.UploadedCounter.WithLabelValues(node.Name()).Inc() + } + elapsed := time.Since(start) + + c.metrics.UploadTimeHistogram.Observe(elapsed.Seconds()) + c.logger.Infof("node %s: streamed %d chunks in %v", node.Name(), len(chunks), elapsed) + + return chunks, nil +} + +// streamDownload requests every chunk over a single websocket connection and +// verifies the deliveries. Responses arrive out of order, so they are matched +// by the address each delivery carries. +func (c *Check) streamDownload(ctx context.Context, node *bee.Client, chunks map[string]bee.Chunk, o Options) error { + stream, err := node.API().ChunkStream.NewDownloadStream(ctx) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer func() { + if err := stream.Close(); err != nil { + c.logger.Debugf("node %s: closing download stream: %v", node.Name(), err) + } + }() + + addrs := make([]swarm.Address, 0, len(chunks)) + for _, chunk := range chunks { + addrs = append(addrs, chunk.Address()) + } + + start := time.Now() + for i := 0; i < len(addrs); i += o.RequestBatchSize { + end := min(i+o.RequestBatchSize, len(addrs)) + if err := stream.Request(addrs[i:end], o.RequestTimeout); err != nil { + return err + } + } + + // Exactly one delivery is expected per requested address. + pending := make(map[string]struct{}, len(chunks)) + for addr := range chunks { + pending[addr] = struct{}{} + } + + for len(pending) > 0 { + delivery, err := stream.Receive(o.RequestTimeout) + if err != nil { + return fmt.Errorf("%d of %d deliveries outstanding: %w", len(pending), len(chunks), err) + } + + addr := delivery.Address.String() + chunk, known := chunks[addr] + if !known { + return fmt.Errorf("%w: delivery for address %s which was never requested", errChunkStream, addr) + } + if _, outstanding := pending[addr]; !outstanding { + return fmt.Errorf("%w: duplicate delivery for address %s", errChunkStream, addr) + } + delete(pending, addr) + + if delivery.Status != api.ChunkDeliverySuccess { + c.metrics.NotRetrievedCounter.WithLabelValues(node.Name()).Inc() + return fmt.Errorf("%w: address %s: expected status 0x%02x, got 0x%02x", errChunkStream, addr, api.ChunkDeliverySuccess, delivery.Status) + } + if !bytes.Equal(delivery.Data, chunk.Data()) { + c.metrics.NotRetrievedCounter.WithLabelValues(node.Name()).Inc() + return fmt.Errorf("%w: address %s: downloaded %d bytes, expected %d", errChunkStream, addr, len(delivery.Data), len(chunk.Data())) + } + + c.metrics.DownloadedCounter.WithLabelValues(node.Name()).Inc() + } + elapsed := time.Since(start) + + c.metrics.DownloadTimeHistogram.Observe(elapsed.Seconds()) + c.logger.Infof("node %s: retrieved %d chunks in %v", node.Name(), len(chunks), elapsed) + + return nil +} + +// checkNotFound asserts that an address that was never uploaded comes back as a +// not-found delivery rather than as data, an error, or silence. This exercises +// the path where retrieval exhausts every peer. +func (c *Check) checkNotFound(ctx context.Context, node *bee.Client, rnd *rand.Rand, o Options) error { + // A freshly generated chunk that is never uploaded gives a valid address + // that no node can hold. + unknown, err := bee.NewRandomChunk(rnd, c.logger) + if err != nil { + return fmt.Errorf("create chunk: %w", err) + } + missing := unknown.Address() + + stream, err := node.API().ChunkStream.NewDownloadStream(ctx) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer func() { + if err := stream.Close(); err != nil { + c.logger.Debugf("node %s: closing download stream: %v", node.Name(), err) + } + }() + + if err := stream.Request([]swarm.Address{missing}, o.RequestTimeout); err != nil { + return err + } + + delivery, err := stream.Receive(o.RequestTimeout) + if err != nil { + return fmt.Errorf("address %s: %w", missing, err) + } + if !delivery.Address.Equal(missing) { + return fmt.Errorf("%w: expected a delivery for %s, got one for %s", errChunkStream, missing, delivery.Address) + } + if delivery.Status != api.ChunkDeliveryNotFound { + return fmt.Errorf("%w: address %s: expected status 0x%02x, got 0x%02x", errChunkStream, missing, api.ChunkDeliveryNotFound, delivery.Status) + } + + c.metrics.NotFoundCounter.WithLabelValues(node.Name()).Inc() + c.logger.Infof("node %s: unknown chunk correctly reported as not found", node.Name()) + + return nil +} diff --git a/pkg/check/chunkstream/metrics.go b/pkg/check/chunkstream/metrics.go new file mode 100644 index 00000000..f6078a4f --- /dev/null +++ b/pkg/check/chunkstream/metrics.go @@ -0,0 +1,89 @@ +package chunkstream + +import ( + m "github.com/ethersphere/beekeeper/pkg/metrics" + "github.com/prometheus/client_golang/prometheus" +) + +type metrics struct { + UploadedCounter *prometheus.CounterVec + UploadErrorCounter *prometheus.CounterVec + UploadTimeHistogram prometheus.Histogram + DownloadedCounter *prometheus.CounterVec + DownloadTimeHistogram prometheus.Histogram + NotRetrievedCounter *prometheus.CounterVec + NotFoundCounter *prometheus.CounterVec +} + +func newMetrics() metrics { + subsystem := "check_chunkstream" + return metrics{ + UploadedCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_uploaded_count", + Help: "Number of chunks uploaded over the chunk stream.", + }, + []string{"node"}, + ), + UploadErrorCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_upload_error_count", + Help: "Number of chunks that failed to upload over the chunk stream.", + }, + []string{"node"}, + ), + UploadTimeHistogram: prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_upload_seconds", + Help: "Time to stream a full batch of chunks up.", + Buckets: prometheus.LinearBuckets(0, 0.5, 10), + }, + ), + DownloadedCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_downloaded_count", + Help: "Number of chunks retrieved over the chunk stream.", + }, + []string{"node"}, + ), + DownloadTimeHistogram: prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_download_seconds", + Help: "Time to stream a full batch of chunks back down.", + Buckets: prometheus.LinearBuckets(0, 0.5, 10), + }, + ), + NotRetrievedCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_not_retrieved_count", + Help: "Number of chunks that were requested but not delivered correctly.", + }, + []string{"node"}, + ), + NotFoundCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: m.Namespace, + Subsystem: subsystem, + Name: "chunks_not_found_count", + Help: "Number of unknown chunks correctly reported as not found.", + }, + []string{"node"}, + ), + } +} + +func (c *Check) Report() []prometheus.Collector { + return m.PrometheusCollectorsFromFields(c.metrics) +} diff --git a/pkg/config/check.go b/pkg/config/check.go index 541afb7e..d63d6523 100644 --- a/pkg/config/check.go +++ b/pkg/config/check.go @@ -12,6 +12,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/check/autotls" "github.com/ethersphere/beekeeper/pkg/check/balances" "github.com/ethersphere/beekeeper/pkg/check/cashout" + "github.com/ethersphere/beekeeper/pkg/check/chunkstream" "github.com/ethersphere/beekeeper/pkg/check/datadurability" "github.com/ethersphere/beekeeper/pkg/check/feed" "github.com/ethersphere/beekeeper/pkg/check/fileretrieval" @@ -386,6 +387,32 @@ var Checks = map[string]CheckType{ return opts, nil }, }, + "chunk-stream": { + NewAction: chunkstream.NewCheck, + NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) { + checkOpts := new(struct { + ChunksPerNode *int `yaml:"chunks-per-node"` + PostageTTL *time.Duration `yaml:"postage-ttl"` + PostageDepth *uint64 `yaml:"postage-depth"` + PostageLabel *string `yaml:"postage-label"` + RequestBatchSize *int `yaml:"request-batch-size"` + RequestTimeout *time.Duration `yaml:"request-timeout"` + Seed *int64 `yaml:"seed"` + SkipNotFound *bool `yaml:"skip-not-found"` + UploadNodeCount *int `yaml:"upload-node-count"` + }) + if err := check.Options.Decode(checkOpts); err != nil { + return nil, fmt.Errorf("decoding check %s options: %w", check.Type, err) + } + opts := chunkstream.NewDefaultOptions() + + if err := applyCheckConfig(checkGlobalConfig, checkOpts, &opts); err != nil { + return nil, fmt.Errorf("applying options: %w", err) + } + + return opts, nil + }, + }, "retrieval": { NewAction: retrieval.NewCheck, NewOptions: func(checkGlobalConfig CheckGlobalConfig, check Check) (any, error) {