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 }