mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
f1abb92f0c
Main motivation is move towards a common backend format for all different types of modes in MinIO, allowing for a simpler code and predictable behavior across all features. This PR also brings features such as versioning, replication, transitioning to single drive setups.
388 lines
10 KiB
Go
388 lines
10 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/minio/minio/internal/config"
|
|
"github.com/minio/minio/internal/lock"
|
|
"github.com/minio/minio/internal/logger"
|
|
)
|
|
|
|
// FS format version strings.
|
|
const (
|
|
formatBackendFS = "fs"
|
|
formatFSVersionV1 = "1"
|
|
formatFSVersionV2 = "2"
|
|
)
|
|
|
|
// formatFSV1 - structure holds format version '1'.
|
|
type formatFSV1 struct {
|
|
formatMetaV1
|
|
FS struct {
|
|
Version string `json:"version"`
|
|
} `json:"fs"`
|
|
}
|
|
|
|
// formatFSV2 - structure is same as formatFSV1. But the multipart backend
|
|
// structure is flat instead of hierarchy now.
|
|
// In .minio.sys/multipart we have:
|
|
// sha256(bucket/object)/uploadID/[fs.json, 1.etag, 2.etag ....]
|
|
type formatFSV2 = formatFSV1
|
|
|
|
// Used to detect the version of "fs" format.
|
|
type formatFSVersionDetect struct {
|
|
FS struct {
|
|
Version string `json:"version"`
|
|
} `json:"fs"`
|
|
}
|
|
|
|
// Generic structure to manage both v1 and v2 structures
|
|
type formatFS struct {
|
|
formatMetaV1
|
|
FS interface{} `json:"fs"`
|
|
}
|
|
|
|
// Returns the latest "fs" format V1
|
|
func newFormatFSV1() (format *formatFSV1) {
|
|
f := &formatFSV1{}
|
|
f.Version = formatMetaVersionV1
|
|
f.Format = formatBackendFS
|
|
f.ID = mustGetUUID()
|
|
f.FS.Version = formatFSVersionV1
|
|
return f
|
|
}
|
|
|
|
// Returns the field formatMetaV1.Format i.e the string "fs" which is never likely to change.
|
|
// We do not use this function in Erasure to get the format as the file is not fcntl-locked on Erasure.
|
|
func formatMetaGetFormatBackendFS(r io.ReadSeeker) (string, error) {
|
|
format := &formatMetaV1{}
|
|
if err := jsonLoad(r, format); err != nil {
|
|
return "", err
|
|
}
|
|
if format.Version == formatMetaVersionV1 {
|
|
return format.Format, nil
|
|
}
|
|
return "", fmt.Errorf(`format.Version expected: %s, got: %s`, formatMetaVersionV1, format.Version)
|
|
}
|
|
|
|
// Returns formatFS.FS.Version
|
|
func formatFSGetVersion(r io.ReadSeeker) (string, error) {
|
|
format := &formatFSVersionDetect{}
|
|
if err := jsonLoad(r, format); err != nil {
|
|
return "", err
|
|
}
|
|
return format.FS.Version, nil
|
|
}
|
|
|
|
// Migrate from V1 to V2. V2 implements new backend format for multipart
|
|
// uploads. Delete the previous multipart directory.
|
|
func formatFSMigrateV1ToV2(ctx context.Context, wlk *lock.LockedFile, fsPath string) error {
|
|
version, err := formatFSGetVersion(wlk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if version != formatFSVersionV1 {
|
|
return fmt.Errorf(`format.json version expected %s, found %s`, formatFSVersionV1, version)
|
|
}
|
|
|
|
if err = fsRemoveAll(ctx, path.Join(fsPath, minioMetaMultipartBucket)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = os.MkdirAll(path.Join(fsPath, minioMetaMultipartBucket), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
formatV1 := formatFSV1{}
|
|
if err = jsonLoad(wlk, &formatV1); err != nil {
|
|
return err
|
|
}
|
|
|
|
formatV2 := formatFSV2{}
|
|
formatV2.formatMetaV1 = formatV1.formatMetaV1
|
|
formatV2.FS.Version = formatFSVersionV2
|
|
|
|
return jsonSave(wlk.File, formatV2)
|
|
}
|
|
|
|
// Migrate the "fs" backend.
|
|
// Migration should happen when formatFSV1.FS.Version changes. This version
|
|
// can change when there is a change to the struct formatFSV1.FS or if there
|
|
// is any change in the backend file system tree structure.
|
|
func formatFSMigrate(ctx context.Context, wlk *lock.LockedFile, fsPath string) error {
|
|
// Add any migration code here in case we bump format.FS.Version
|
|
version, err := formatFSGetVersion(wlk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch version {
|
|
case formatFSVersionV1:
|
|
if err = formatFSMigrateV1ToV2(ctx, wlk, fsPath); err != nil {
|
|
return err
|
|
}
|
|
fallthrough
|
|
case formatFSVersionV2:
|
|
// We are at the latest version.
|
|
}
|
|
|
|
// Make sure that the version is what we expect after the migration.
|
|
version, err = formatFSGetVersion(wlk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if version != formatFSVersionV2 {
|
|
return config.ErrUnexpectedBackendVersion(fmt.Errorf(`%s file: expected FS version: %s, found FS version: %s`, formatConfigFile, formatFSVersionV2, version))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Creates a new format.json if unformatted.
|
|
func createFormatFS(fsFormatPath string) error {
|
|
// Attempt a write lock on formatConfigFile `format.json`
|
|
// file stored in minioMetaBucket(.minio.sys) directory.
|
|
lk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR|os.O_CREATE, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Close the locked file upon return.
|
|
defer lk.Close()
|
|
|
|
fi, err := lk.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fi.Size() != 0 {
|
|
// format.json already got created because of another minio process's createFormatFS()
|
|
return nil
|
|
}
|
|
|
|
return jsonSave(lk.File, newFormatFSV1())
|
|
}
|
|
|
|
// This function returns a read-locked format.json reference to the caller.
|
|
// The file descriptor should be kept open throughout the life
|
|
// of the process so that another minio process does not try to
|
|
// migrate the backend when we are actively working on the backend.
|
|
func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, err error) {
|
|
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
|
|
|
|
// Add a deployment ID, if it does not exist.
|
|
if err := formatFSFixDeploymentID(ctx, fsFormatPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Any read on format.json should be done with read-lock.
|
|
// Any write on format.json should be done with write-lock.
|
|
for {
|
|
isEmpty := false
|
|
rlk, err := lock.RLockedOpenFile(fsFormatPath)
|
|
if err == nil {
|
|
// format.json can be empty in a rare condition when another
|
|
// minio process just created the file but could not hold lock
|
|
// and write to it.
|
|
var fi os.FileInfo
|
|
fi, err = rlk.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isEmpty = fi.Size() == 0
|
|
}
|
|
if osIsNotExist(err) || isEmpty {
|
|
if err == nil {
|
|
rlk.Close()
|
|
}
|
|
// Fresh disk - create format.json
|
|
err = createFormatFS(fsFormatPath)
|
|
if err == lock.ErrAlreadyLocked {
|
|
// Lock already present, sleep and attempt again.
|
|
// Can happen in a rare situation when a parallel minio process
|
|
// holds the lock and creates format.json
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// After successfully creating format.json try to hold a read-lock on
|
|
// the file.
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
formatBackend, err := formatMetaGetFormatBackendFS(rlk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if formatBackend == formatBackendErasureSingle {
|
|
return nil, errFreshDisk
|
|
}
|
|
if formatBackend != formatBackendFS {
|
|
return nil, fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
|
|
}
|
|
version, err := formatFSGetVersion(rlk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if version != formatFSVersionV2 {
|
|
// Format needs migration
|
|
rlk.Close()
|
|
// Hold write lock during migration so that we do not disturb any
|
|
// minio processes running in parallel.
|
|
var wlk *lock.LockedFile
|
|
wlk, err = lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
|
if err == lock.ErrAlreadyLocked {
|
|
// Lock already present, sleep and attempt again.
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = formatFSMigrate(ctx, wlk, fsPath)
|
|
wlk.Close()
|
|
if err != nil {
|
|
// Migration failed, bail out so that the user can observe what happened.
|
|
return nil, err
|
|
}
|
|
// Successfully migrated, now try to hold a read-lock on format.json
|
|
continue
|
|
}
|
|
var id string
|
|
if id, err = formatFSGetDeploymentID(rlk); err != nil {
|
|
rlk.Close()
|
|
return nil, err
|
|
}
|
|
globalDeploymentID = id
|
|
return rlk, nil
|
|
}
|
|
}
|
|
|
|
func formatFSGetDeploymentID(rlk *lock.RLockedFile) (id string, err error) {
|
|
format := &formatFS{}
|
|
if err := jsonLoad(rlk, format); err != nil {
|
|
return "", err
|
|
}
|
|
return format.ID, nil
|
|
}
|
|
|
|
// Generate a deployment ID if one does not exist already.
|
|
func formatFSFixDeploymentID(ctx context.Context, fsFormatPath string) error {
|
|
rlk, err := lock.RLockedOpenFile(fsFormatPath)
|
|
if err == nil {
|
|
// format.json can be empty in a rare condition when another
|
|
// minio process just created the file but could not hold lock
|
|
// and write to it.
|
|
var fi os.FileInfo
|
|
fi, err = rlk.Stat()
|
|
if err != nil {
|
|
rlk.Close()
|
|
return err
|
|
}
|
|
if fi.Size() == 0 {
|
|
rlk.Close()
|
|
return nil
|
|
}
|
|
}
|
|
if osIsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
formatBackend, err := formatMetaGetFormatBackendFS(rlk)
|
|
if err != nil {
|
|
rlk.Close()
|
|
return err
|
|
}
|
|
if formatBackend == formatBackendErasureSingle {
|
|
rlk.Close()
|
|
return errFreshDisk
|
|
}
|
|
if formatBackend != formatBackendFS {
|
|
rlk.Close()
|
|
return fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
|
|
}
|
|
|
|
format := &formatFS{}
|
|
err = jsonLoad(rlk, format)
|
|
rlk.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if it needs to be updated
|
|
if format.ID != "" {
|
|
return nil
|
|
}
|
|
|
|
formatStartTime := time.Now().Round(time.Second)
|
|
getElapsedTime := func() string {
|
|
return time.Now().Round(time.Second).Sub(formatStartTime).String()
|
|
}
|
|
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
var wlk *lock.LockedFile
|
|
var stop bool
|
|
for !stop {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("Initializing FS format stopped gracefully")
|
|
default:
|
|
wlk, err = lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
|
if err == lock.ErrAlreadyLocked {
|
|
// Lock already present, sleep and attempt again
|
|
logger.Info("Another minio process(es) might be holding a lock to the file %s. Please kill that minio process(es) (elapsed %s)\n", fsFormatPath, getElapsedTime())
|
|
time.Sleep(time.Duration(r.Float64() * float64(5*time.Second)))
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
stop = true
|
|
}
|
|
defer wlk.Close()
|
|
|
|
if err = jsonLoad(wlk, format); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if format needs to be updated
|
|
if format.ID != "" {
|
|
return nil
|
|
}
|
|
|
|
// Set new UUID to the format and save it
|
|
format.ID = mustGetUUID()
|
|
return jsonSave(wlk, format)
|
|
}
|