Commit ae260514 authored by ale's avatar ale

Support sharding using standard go-common/clientutil

parent e7b6f531
Pipeline #721 passed with stages
in 41 seconds
......@@ -2,137 +2,57 @@ package client
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/url"
"sync"
"time"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/keystore"
)
// Client for the keystore API (for a specific backend).
type Client struct {
*http.Client
backendURL string
// Client for the keystore API.
type Client interface {
Open(context.Context, string, string, string, int) error
Get(context.Context, string, string, string) ([]byte, error)
Close(context.Context, string, string) error
}
// Config for a keystore client.
type Config struct {
BackendURL string `yaml:"backend_url"`
TLSConfig *clientutil.TLSClientConfig `yaml:"tls_config"`
type ksClient struct {
be clientutil.Backend
}
// New returns a new Client with the given Config. Use this when the
// keystore service runs on a single global instance.
func New(config *Config) (*Client, error) {
u, err := url.Parse(config.BackendURL)
func New(config *clientutil.BackendConfig) (*ksClient, error) {
be, err := clientutil.NewBackend(config)
if err != nil {
return nil, err
}
var tlsConfig *tls.Config
if config.TLSConfig != nil {
tlsConfig, err = config.TLSConfig.TLSConfig()
if err != nil {
return nil, err
}
}
c := &http.Client{
Transport: clientutil.NewTransport([]string{u.Host}, tlsConfig, nil),
Timeout: 20 * time.Second,
}
return &Client{
Client: c,
backendURL: config.BackendURL,
}, nil
return &ksClient{be}, nil
}
func (c *Client) Open(ctx context.Context, username, password string, ttl int) error {
func (c *ksClient) Open(ctx context.Context, shard, username, password string, ttl int) error {
req := keystore.OpenRequest{
Username: username,
Password: password,
TTL: ttl,
}
var resp keystore.OpenResponse
return clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/open", &req, &resp)
return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/open", &req, &resp)
}
func (c *Client) Get(ctx context.Context, username, ssoTicket string) ([]byte, error) {
func (c *ksClient) Get(ctx context.Context, shard, username, ssoTicket string) ([]byte, error) {
req := keystore.GetRequest{
Username: username,
SSOTicket: ssoTicket,
}
var resp keystore.GetResponse
err := clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/get", &req, &resp)
err := clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/get", &req, &resp)
return resp.Key, err
}
func (c *Client) Close(ctx context.Context, username string) error {
func (c *ksClient) Close(ctx context.Context, shard, username string) error {
req := keystore.CloseRequest{
Username: username,
}
var resp keystore.CloseResponse
return clientutil.DoJSONHTTPRequest(ctx, c.Client, c.backendURL+"/api/close", &req, &resp)
}
// ShardedClient for the keystore API (sharded service).
type ShardedClient struct {
baseURL *url.URL
tlsConfig *tls.Config
mx sync.Mutex
shards map[string]*Client
}
// NewSharded creates a ShardedClient for the keystore service. Use it
// when the service is partitioned (sharded) across multiple backends.
func NewSharded(config *Config) (*ShardedClient, error) {
u, err := url.Parse(config.BackendURL)
if err != nil {
return nil, err
}
var tlsConfig *tls.Config
if config.TLSConfig != nil {
tlsConfig, err = config.TLSConfig.TLSConfig()
if err != nil {
return nil, err
}
}
return &ShardedClient{
baseURL: u,
tlsConfig: tlsConfig,
shards: make(map[string]*Client),
}, nil
}
func (c *ShardedClient) getShardURL(shard string) *url.URL {
u := *c.baseURL
u.Host = fmt.Sprintf("%s.%s", shard, u.Host)
return &u
}
// Shard returns the Client for a specific service shard.
func (c *ShardedClient) Shard(shard string) *Client {
c.mx.Lock()
defer c.mx.Unlock()
client, ok := c.shards[shard]
if !ok {
u := c.getShardURL(shard)
client = &Client{
Client: &http.Client{
Transport: clientutil.NewTransport([]string{u.Host}, c.tlsConfig, nil),
Timeout: 20 * time.Second,
},
backendURL: u.String(),
}
c.shards[shard] = client
}
return client
return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/close", &req, &resp)
}
package clientutil
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"sync"
"time"
)
// BackendConfig specifies the configuration to access a service.
//
// Services with multiple backends can be replicated or partitioned,
// depending on a configuration switch, making it a deployment-time
// decision. Clients are expected to compute their own sharding
// function (either by database lookup or other methods), and expose a
// 'shard' parameter on their APIs.
type BackendConfig struct {
URL string `yaml:"url"`
Sharded bool `yaml:"sharded"`
TLSConfig *TLSClientConfig `yaml:"tls_config"`
}
// Backend is a runtime class that provides http Clients for use with
// a specific service backend. If the service can't be partitioned,
// pass an empty string to the Client method.
type Backend interface {
// URL for the service for a specific shard.
URL(string) string
// Client that can be used to make a request to the service.
Client(string) *http.Client
}
// NewBackend returns a new Backend with the given config.
func NewBackend(config *BackendConfig) (Backend, error) {
u, err := url.Parse(config.URL)
if err != nil {
return nil, err
}
var tlsConfig *tls.Config
if config.TLSConfig != nil {
tlsConfig, err = config.TLSConfig.TLSConfig()
if err != nil {
return nil, err
}
}
if config.Sharded {
return &replicatedClient{
u: u,
c: newHTTPClient(u, tlsConfig),
}, nil
}
return &shardedClient{
baseURL: u,
tlsConfig: tlsConfig,
urls: make(map[string]*url.URL),
shards: make(map[string]*http.Client),
}, nil
}
type replicatedClient struct {
c *http.Client
u *url.URL
}
func (r *replicatedClient) Client(_ string) *http.Client { return r.c }
func (r *replicatedClient) URL(_ string) string { return r.u.String() }
type shardedClient struct {
baseURL *url.URL
tlsConfig *tls.Config
mx sync.Mutex
urls map[string]*url.URL
shards map[string]*http.Client
}
func (s *shardedClient) getShardURL(shard string) *url.URL {
if shard == "" {
return s.baseURL
}
u, ok := s.urls[shard]
if !ok {
var tmp = *s.baseURL
tmp.Host = fmt.Sprintf("%s.%s", shard, tmp.Host)
u = &tmp
s.urls[shard] = u
}
return u
}
func (s *shardedClient) URL(shard string) string {
s.mx.Lock()
defer s.mx.Unlock()
return s.getShardURL(shard).String()
}
func (s *shardedClient) Client(shard string) *http.Client {
s.mx.Lock()
defer s.mx.Unlock()
client, ok := s.shards[shard]
if !ok {
u := s.getShardURL(shard)
client = newHTTPClient(u, s.tlsConfig)
s.shards[shard] = client
}
return client
}
func newHTTPClient(u *url.URL, tlsConfig *tls.Config) *http.Client {
return &http.Client{
Transport: NewTransport([]string{u.Host}, tlsConfig, nil),
Timeout: 30 * time.Second,
}
}
......@@ -9,10 +9,10 @@
"revisionTime": "2017-12-14T08:46:15Z"
},
{
"checksumSHA1": "2X2UMundICtpGTb8pTdBk7PCKss=",
"checksumSHA1": "o+rWKVQIDy79ZwrItwa5/whAL6g=",
"path": "git.autistici.org/ai3/go-common/clientutil",
"revision": "0cc062297e2c27f9a1abcb1a00172d1e0281f8cb",
"revisionTime": "2017-12-14T08:46:15Z"
"revision": "9b20acad90c411c48f7ddc837a35ef3d0d6f98d4",
"revisionTime": "2017-12-17T20:32:41Z"
},
{
"checksumSHA1": "mEnXMNziH82HFtGngHU19VHTVHs=",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment