From fb908275328cb482a62f3eccb5a6902ca892b9b7 Mon Sep 17 00:00:00 2001
From: Alex Vanin <alexey@nspcc.ru>
Date: Wed, 18 Nov 2020 16:01:59 +0300
Subject: [PATCH] [#184] Add SDK client cache package

With this package we can reuse already created connections.
Later on neofs-api-go will support checking connection health
and `Close` operation, so this cache could run worker and remove
unhealthy clients.

Signed-off-by: Alex Vanin <alexey@nspcc.ru>
---
 pkg/network/cache/client.go | 69 +++++++++++++++++++++++++++++++++++++
 1 file changed, 69 insertions(+)
 create mode 100644 pkg/network/cache/client.go

diff --git a/pkg/network/cache/client.go b/pkg/network/cache/client.go
new file mode 100644
index 000000000..136b84dea
--- /dev/null
+++ b/pkg/network/cache/client.go
@@ -0,0 +1,69 @@
+package cache
+
+import (
+	"crypto/ecdsa"
+	"crypto/sha256"
+	"encoding/hex"
+	"sync"
+
+	"github.com/nspcc-dev/neofs-api-go/pkg/client"
+	crypto "github.com/nspcc-dev/neofs-crypto"
+)
+
+type (
+	// ClientCache is a structure around neofs-api-go/pkg/client to reuse
+	// already created clients.
+	ClientCache struct {
+		mu      *sync.RWMutex
+		clients map[string]*client.Client
+	}
+)
+
+// NewSDKClientCache creates instance of client cache.
+func NewSDKClientCache() *ClientCache {
+	return &ClientCache{
+		mu:      new(sync.RWMutex),
+		clients: make(map[string]*client.Client),
+	}
+}
+
+// Get function returns existing client or creates a new one. Consider passing
+// connection options to specify details for client, but don't forget that two
+// different set of options should provide two different clients.
+func (c *ClientCache) Get(key *ecdsa.PrivateKey, address string) (*client.Client, error) {
+	id := uniqueID(key, address)
+
+	c.mu.RLock()
+	if cli, ok := c.clients[id]; ok {
+		// todo: check underlying connection neofs-api-go#196
+		c.mu.RUnlock()
+
+		return cli, nil
+	}
+
+	c.mu.RUnlock()
+	// if client is not found in cache, then create a new one
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	// check once again if client is missing in cache, concurrent routine could
+	// create client while this routine was locked on `c.mu.Lock()`.
+	if cli, ok := c.clients[id]; ok {
+		return cli, nil
+	}
+
+	cli, err := client.New(key, client.WithAddress(address))
+	if err != nil {
+		return nil, err
+	}
+
+	c.clients[id] = cli
+
+	return cli, nil
+}
+
+func uniqueID(key *ecdsa.PrivateKey, address string) string {
+	keyFingerprint := sha256.Sum256(crypto.MarshalPrivateKey(key))
+
+	return hex.EncodeToString(keyFingerprint[:]) + address
+}