github.com/bcskill/bcschain/v3@v3.4.9-beta2/ethdb/s3/s3.go (about)

     1  package s3
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"sort"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/bcskill/bcschain/v3/ethdb"
    15  	"github.com/bcskill/bcschain/v3/log"
    16  	"github.com/bcskill/bcschain/v3/metrics"
    17  	"github.com/minio/minio-go"
    18  )
    19  
    20  var (
    21  	uploadBytesMeter   = metrics.NewRegisteredMeter("ethdb/s3/upload/bytes", nil)
    22  	downloadBytesMeter = metrics.NewRegisteredMeter("ethdb/s3/download/bytes", nil)
    23  )
    24  
    25  const (
    26  	// FGetObjectInterval represents the time between attempts to successfully
    27  	// fetch objects from the S3 store.
    28  	FGetObjectInterval = 2 * time.Second
    29  )
    30  
    31  // ConfigureDB updates db to archive to S3 if S3 configuration enabled.
    32  func ConfigureDB(db *ethdb.DB, config ethdb.Config) error {
    33  	if config.Endpoint == "" || config.Bucket == "" {
    34  		return nil
    35  	}
    36  
    37  	c := NewClient()
    38  	c.Endpoint = config.Endpoint
    39  	c.Bucket = config.Bucket
    40  	c.AccessKeyID = config.AccessKeyID
    41  	c.SecretAccessKey = config.SecretAccessKey
    42  	if err := c.Open(); err != nil {
    43  		log.Error("Cannot open S3 client", "err", err)
    44  		return err
    45  	}
    46  
    47  	db.SegmentOpener = NewSegmentOpener(c)
    48  	db.SegmentCompactor = NewSegmentCompactor(c)
    49  
    50  	return nil
    51  }
    52  
    53  // Client represents a client to an S3 compatible bucket.
    54  type Client struct {
    55  	client *minio.Client
    56  
    57  	// Connection information for S3-compatible bucket.
    58  	// Must be set before calling Open().
    59  	Endpoint string
    60  	Bucket   string
    61  
    62  	// Authentication for S3-compatible bucket.
    63  	// Must be set before calling Open().
    64  	AccessKeyID     string
    65  	SecretAccessKey string
    66  }
    67  
    68  // NewClient returns a new instance of Client.
    69  func NewClient() *Client {
    70  	return &Client{}
    71  }
    72  
    73  func (c *Client) Open() (err error) {
    74  	// Create minio client.
    75  	ssl := !strings.HasPrefix(c.Endpoint, "127.0.0.1:")
    76  	if c.client, err = minio.New(c.Endpoint, c.AccessKeyID, c.SecretAccessKey, ssl); err != nil {
    77  		log.Error("Cannot create minio client", "err", err)
    78  		return err
    79  	}
    80  
    81  	// Verify bucket exists.
    82  	if ok, err := c.client.BucketExists(c.Bucket); err != nil {
    83  		log.Error("Cannot verify bucket", "err", err)
    84  		return err
    85  	} else if !ok {
    86  		return fmt.Errorf("ethdb/s3: bucket does not exist: %s", c.Bucket)
    87  	}
    88  	return nil
    89  }
    90  
    91  // ListObjectKeys returns a list of all object keys with a given prefix.
    92  func (c *Client) ListObjectKeys(prefix string) ([]string, error) {
    93  	log.Info("List s3 keys", "prefix", prefix)
    94  
    95  	var keys []string
    96  	for info := range c.client.ListObjects(c.Bucket, prefix, true, nil) {
    97  		if info.Err != nil {
    98  			return nil, info.Err
    99  		}
   100  		keys = append(keys, info.Key)
   101  	}
   102  	return keys, nil
   103  }
   104  
   105  // FGetObject fetches the object at key and atomically writes it to path.
   106  // Attempts multiple times until a successful fetch has been acheived.
   107  func (c *Client) FGetObject(ctx context.Context, key, path string) (err error) {
   108  	const retry = 5
   109  	for i := 0; i < retry; i++ {
   110  		if err = c.tryFGetObject(ctx, key, path); err == nil {
   111  			return nil
   112  		}
   113  		log.Error("Error fetching S3 file segment", "i", i, "path", path, "err", err)
   114  		time.Sleep(FGetObjectInterval)
   115  	}
   116  	return err
   117  }
   118  
   119  func (c *Client) tryFGetObject(ctx context.Context, key, path string) (err error) {
   120  	tmpPath := path + ".tmp"
   121  	if err := c.client.FGetObjectWithContext(ctx, c.Bucket, key, tmpPath, minio.GetObjectOptions{}); err != nil {
   122  		return err
   123  	}
   124  
   125  	// Measure size downloaded.
   126  	if fi, err := os.Stat(tmpPath); err != nil {
   127  		return err
   128  	} else {
   129  		downloadBytesMeter.Mark(fi.Size())
   130  	}
   131  
   132  	// Verify file segment checksum matches computed.
   133  	if err := ethdb.VerifyFileSegment(tmpPath); err != nil {
   134  		os.Remove(tmpPath)
   135  		return err
   136  	}
   137  
   138  	// Move file from temp path to actual path.
   139  	if err := os.Rename(tmpPath, path); err != nil {
   140  		os.Remove(tmpPath)
   141  		return err
   142  	}
   143  	return nil
   144  }
   145  
   146  // PutObject writes an object to a key.
   147  func (c *Client) PutObject(ctx context.Context, key string, value []byte) (n int64, err error) {
   148  	n, err = c.client.PutObjectWithContext(ctx, c.Bucket, key, bytes.NewReader(value), int64(len(value)), minio.PutObjectOptions{})
   149  	uploadBytesMeter.Mark(n)
   150  	return n, err
   151  }
   152  
   153  // FPutObject writes an object to key from a file at path.
   154  func (c *Client) FPutObject(ctx context.Context, key, path string) (n int64, err error) {
   155  	n, err = c.client.FPutObjectWithContext(ctx, c.Bucket, key, path, minio.PutObjectOptions{})
   156  	uploadBytesMeter.Mark(n)
   157  	return n, err
   158  }
   159  
   160  // RemoveObject removes an object by key.
   161  func (c *Client) RemoveObject(ctx context.Context, key string) error {
   162  	return c.client.RemoveObject(c.Bucket, key)
   163  }
   164  
   165  // Segment represents an ethdb.FileSegment stored in S3.
   166  type Segment struct {
   167  	mu       sync.RWMutex
   168  	muEnsure sync.Mutex // lock during check for file existence.
   169  
   170  	client  *Client
   171  	segment *ethdb.FileSegment
   172  	table   string // table name
   173  	name    string // segment name
   174  	path    string // local path
   175  }
   176  
   177  // NewSegment returns a new instance of Segment.
   178  func NewSegment(client *Client, table, name, path string) *Segment {
   179  	return &Segment{
   180  		client: client,
   181  		table:  table,
   182  		name:   name,
   183  		path:   path,
   184  	}
   185  }
   186  
   187  // Name returns the name of the segment.
   188  func (s *Segment) Name() string { return s.name }
   189  
   190  // Path returns the local path of the segment.
   191  func (s *Segment) Path() string { return s.path }
   192  
   193  // Close closes the underlying file segment.
   194  func (s *Segment) Close() error {
   195  	s.mu.Lock()
   196  	defer s.mu.Unlock()
   197  	if s.segment != nil {
   198  		return s.segment.Close()
   199  	}
   200  	return nil
   201  }
   202  
   203  // Purge closes the underlying file segment and removes the on-disk file.
   204  func (s *Segment) Purge() error {
   205  	s.mu.Lock()
   206  	defer s.mu.Unlock()
   207  
   208  	if s.segment == nil {
   209  		return nil
   210  	}
   211  
   212  	if err := s.segment.Close(); err != nil {
   213  		return err
   214  	} else if err := os.Remove(s.segment.Path()); err != nil {
   215  		return err
   216  	}
   217  	s.segment = nil
   218  
   219  	return nil
   220  }
   221  
   222  // ensureFileSegment instantiates the underlying file segment from the local disk.
   223  // If the segment does not exist locally on disk then it is fetched from S3.
   224  func (s *Segment) ensureFileSegment(ctx context.Context) error {
   225  	s.muEnsure.Lock()
   226  	defer s.muEnsure.Unlock()
   227  
   228  	// Exit if underlying segment exists.
   229  	if s.segment != nil {
   230  		return nil
   231  	}
   232  
   233  	// Fetch segment if it doesn't exist on disk.
   234  	if _, err := os.Stat(s.path); os.IsNotExist(err) {
   235  		log.Info("Fetch segment from s3", "key", SegmentKey(s.table, s.name))
   236  		if err := s.client.FGetObject(ctx, SegmentKey(s.table, s.name), s.path); err != nil {
   237  			log.Error("Cannot fetch segment from s3", "key", SegmentKey(s.table, s.name), "err", err)
   238  			return err
   239  		}
   240  	} else if err != nil {
   241  		return err
   242  	}
   243  
   244  	// Open file segment on the local file.
   245  	s.segment = ethdb.NewFileSegment(s.name, s.path)
   246  	if err := s.segment.Open(); err != nil {
   247  		return err
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  // Has returns true if the key exists.
   254  func (s *Segment) Has(key []byte) (bool, error) {
   255  	s.mu.RLock()
   256  	defer s.mu.RUnlock()
   257  	if err := s.ensureFileSegment(context.TODO()); err != nil {
   258  		return false, err
   259  	}
   260  	return s.segment.Has(key)
   261  }
   262  
   263  // Get returns the value of the given key.
   264  func (s *Segment) Get(key []byte) ([]byte, error) {
   265  	s.mu.RLock()
   266  	defer s.mu.RUnlock()
   267  	if err := s.ensureFileSegment(context.TODO()); err != nil {
   268  		return nil, err
   269  	}
   270  	return s.segment.Get(key)
   271  }
   272  
   273  // Iterator returns an iterator for the segment.
   274  func (s *Segment) Iterator() ethdb.SegmentIterator {
   275  	s.mu.RLock() // unlocked by SegmentIterator.Close()
   276  	return &SegmentIterator{
   277  		SegmentIterator: s.segment.Iterator(),
   278  		segment:         s,
   279  	}
   280  }
   281  
   282  // Ensure implementation implements interface.
   283  var _ ethdb.SegmentIterator = (*SegmentIterator)(nil)
   284  
   285  // SegmentIterator represents a wrapper around ethdb.SegmentIterator.
   286  // Releases read lock on close.
   287  type SegmentIterator struct {
   288  	ethdb.SegmentIterator
   289  	segment *Segment
   290  }
   291  
   292  // Close releases iterator resources and releases the read lock on the segment.
   293  func (itr *SegmentIterator) Close() error {
   294  	itr.segment.mu.RUnlock()
   295  	return itr.SegmentIterator.Close()
   296  }
   297  
   298  // SegmentKey returns the key used for the segment on S3.
   299  func SegmentKey(table, name string) string {
   300  	return path.Join(table, name)
   301  }
   302  
   303  // Ensure implementation fulfills interface.
   304  var _ ethdb.SegmentOpener = (*SegmentOpener)(nil)
   305  
   306  // SegmentOpener opens segments as a s3.Segments.
   307  type SegmentOpener struct {
   308  	Client *Client
   309  }
   310  
   311  // NewSegmentOpener returns a new instance of SegmentOpener.
   312  func NewSegmentOpener(client *Client) *SegmentOpener {
   313  	return &SegmentOpener{Client: client}
   314  }
   315  
   316  // ListSegmentNames returns a list of segment names for a table.
   317  func (o *SegmentOpener) ListSegmentNames(path, table string) ([]string, error) {
   318  	// Fetch local keys.
   319  	localKeys, err := ethdb.NewFileSegmentOpener().ListSegmentNames(path, table)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	// Fetch remote keys.
   325  	remoteKeys, err := o.Client.ListObjectKeys(table)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	for i, key := range remoteKeys {
   330  		remoteKeys[i] = strings.TrimPrefix(key, table+"/")
   331  	}
   332  
   333  	// Merge key sets.
   334  	m := make(map[string]struct{})
   335  	for _, k := range localKeys {
   336  		m[k] = struct{}{}
   337  	}
   338  	for _, k := range remoteKeys {
   339  		m[k] = struct{}{}
   340  	}
   341  
   342  	// Convert to slice.
   343  	a := make([]string, 0, len(m))
   344  	for k := range m {
   345  		a = append(a, k)
   346  	}
   347  	sort.Strings(a)
   348  
   349  	return a, nil
   350  }
   351  
   352  // OpenSegment returns creates and opens a reference to a remote immutable segment.
   353  func (o *SegmentOpener) OpenSegment(table, name, path string) (ethdb.Segment, error) {
   354  	return NewSegment(o.Client, table, name, path), nil
   355  }
   356  
   357  // Ensure implementation fulfills interface.
   358  var _ ethdb.SegmentCompactor = (*SegmentCompactor)(nil)
   359  
   360  // SegmentCompactor wraps ethdb.FileSegmentCompactor and uploads to S3 after compaction.
   361  type SegmentCompactor struct {
   362  	Client *Client
   363  }
   364  
   365  // NewSegmentCompactor returns a new instance of SegmentCompactor.
   366  func NewSegmentCompactor(client *Client) *SegmentCompactor {
   367  	return &SegmentCompactor{
   368  		Client: client,
   369  	}
   370  }
   371  
   372  // CompactSegment compacts s into a FileSegement and uploads it to S3.
   373  func (c *SegmentCompactor) CompactSegment(ctx context.Context, table string, s *ethdb.LDBSegment) (ethdb.Segment, error) {
   374  	fsc := ethdb.NewFileSegmentCompactor()
   375  
   376  	tmpPath := s.Path() + ".tmp"
   377  	if err := fsc.CompactSegmentTo(ctx, s, tmpPath); err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	if _, err := c.Client.FPutObject(ctx, SegmentKey(table, s.Name()), tmpPath); err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	// Close and remove both segments.
   386  	if err := s.Close(); err != nil {
   387  		return nil, err
   388  	} else if err := os.RemoveAll(s.Path()); err != nil {
   389  		return nil, err
   390  	} else if err := os.Remove(tmpPath); err != nil {
   391  		return nil, err
   392  	}
   393  
   394  	return NewSegment(c.Client, table, s.Name(), s.Path()), nil
   395  }
   396  
   397  // UncompactSegment uncompacts s into an LDBSegement.
   398  func (c *SegmentCompactor) UncompactSegment(ctx context.Context, table string, s ethdb.Segment) (*ethdb.LDBSegment, error) {
   399  	return ethdb.NewFileSegmentCompactor().UncompactSegment(ctx, table, s)
   400  }