-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial Meilisearch Support #54
Merged
Merged
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
dc98028
Initial Meilisearch Support
majst01 3ae5995
linter fix
majst01 6915783
linter fix
majst01 479027a
logging
majst01 95d3911
stupid me
majst01 86d4b9e
Fix dump move
majst01 4a8a7b6
Fix cross mountpoint move
majst01 a9f1079
Try native move
majst01 2b65b56
create latest stable dump for upgrades
majst01 408d73b
Upgrade
majst01 98abd10
fix lateststableDump
majst01 8e72438
fix lateststableDump
majst01 0241268
fix lateststableDump
majst01 d55a498
fix lateststableDump
majst01 603093e
semver comparison
majst01 bc3aac8
Ignore existing db
majst01 f90e1d0
Fix upgrade
majst01 562523c
Fix upgrade
majst01 4576437
Fix upgrade
majst01 a335f66
Fix upgrade
majst01 925f6a5
Kill database after upgrade
majst01 17e9489
Kill database after upgrade
majst01 2171467
Errgroup
majst01 6e0e9aa
fixes
majst01 99daca5
refinements
majst01 9fcff68
simplification and implement recover
majst01 884be8c
dump creation fix
majst01 8fadb33
Merge branch 'master' of https://github.com/metal-stack/backup-restor…
majst01 28193e0
Fix backup copy
majst01 da7cc39
Fix backup copy
majst01 6fc7fc9
Fix backup copy
majst01 e6598ac
Merge master
majst01 b7b48ab
Larger linting timeout
majst01 ab9adc2
Initial integration test
majst01 fd2a4a8
Linter fix
majst01 f4283bd
Make Meilisearch upgrade pass
majst01 74b64d7
Update modules
majst01 db5fec9
Raise integration test timeout
majst01 3c444a9
Do not change unrelated
majst01 b1d4da7
Obsolet manifest
majst01 09fea55
First round of review comments
majst01 943ba85
Make CI pass again
majst01 9a934f8
Better explanation of the bad situation
majst01 11e6aa8
Move examples to separate package and enable generating them without …
Gerrit91 7f0dc84
First version of upgrade by saving previous binary, also bug fixes.
Gerrit91 c076b7c
Stabilize integration tests.
Gerrit91 cea623a
Merge remote-tracking branch 'origin/master' into meilisearch-support
Gerrit91 07b4efd
Typo.
Gerrit91 a384aeb
Improvements on example generation.
Gerrit91 9390f6f
Another review with some improvements.
aa2cc72
Better wait for restore.
d39e57b
Reformat table.
423ce08
Remove unnecessary linter exclusion.
5988ef2
Add validation.
Gerrit91 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,269 @@ | ||
package meilisearch | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path" | ||
"path/filepath" | ||
"strings" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/meilisearch/meilisearch-go" | ||
"github.com/spf13/afero" | ||
"golang.org/x/sync/errgroup" | ||
"golang.org/x/sync/semaphore" | ||
|
||
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/utils" | ||
"github.com/metal-stack/backup-restore-sidecar/pkg/constants" | ||
"go.uber.org/zap" | ||
) | ||
|
||
const ( | ||
meilisearchCmd = "meilisearch" | ||
meilisearchVersionFile = "VERSION" | ||
meilisearchDBDir = "data.ms" | ||
latestStableDump = "latest.dump" | ||
) | ||
|
||
// Meilisearch implements the database interface | ||
type Meilisearch struct { | ||
log *zap.SugaredLogger | ||
executor *utils.CmdExecutor | ||
datadir string | ||
copyBinaryAfterBackup bool | ||
|
||
apikey string | ||
client *meilisearch.Client | ||
} | ||
|
||
// New instantiates a new meilisearch database | ||
func New(log *zap.SugaredLogger, datadir string, url string, apikey string) (*Meilisearch, error) { | ||
if url == "" { | ||
return nil, fmt.Errorf("meilisearch api url cannot be empty") | ||
} | ||
if apikey == "" { | ||
return nil, fmt.Errorf("meilisearch api key cannot be empty") | ||
} | ||
|
||
client := meilisearch.NewClient(meilisearch.ClientConfig{ | ||
Host: url, | ||
APIKey: apikey, | ||
}) | ||
|
||
return &Meilisearch{ | ||
log: log, | ||
datadir: datadir, | ||
apikey: apikey, | ||
executor: utils.NewExecutor(log), | ||
client: client, | ||
copyBinaryAfterBackup: true, | ||
}, nil | ||
} | ||
|
||
// Backup takes a dump of meilisearch with the meilisearch client. | ||
func (db *Meilisearch) Backup(ctx context.Context) error { | ||
if err := os.RemoveAll(constants.BackupDir); err != nil { | ||
return fmt.Errorf("could not clean backup directory: %w", err) | ||
} | ||
|
||
if err := os.MkdirAll(constants.BackupDir, 0777); err != nil { | ||
return fmt.Errorf("could not create backup directory: %w", err) | ||
} | ||
|
||
dumpResponse, err := db.client.CreateDump() | ||
if err != nil { | ||
return fmt.Errorf("could not create a dump: %w", err) | ||
} | ||
|
||
db.log.Infow("dump creation triggered", "taskUUID", dumpResponse.TaskUID) | ||
|
||
dumpTask, err := db.client.WaitForTask(dumpResponse.TaskUID, meilisearch.WaitParams{Context: ctx}) | ||
if err != nil { | ||
return err | ||
} | ||
db.log.Infow("dump created successfully", "duration", dumpTask.Duration) | ||
|
||
dumps, err := filepath.Glob(constants.BackupDir + "/*.dump") | ||
if err != nil { | ||
return fmt.Errorf("unable to find dump: %w", err) | ||
} | ||
if len(dumps) != 1 { | ||
return fmt.Errorf("did not find unique dump, found %d", len(dumps)) | ||
} | ||
|
||
// we need to do a copy here and cannot simply rename as the file system is | ||
// mounted by two containers. the dump is created in the database container, | ||
// the copy is done in the backup-restore-sidecar container. os.Rename would | ||
// lead to an error. | ||
|
||
err = utils.Copy(afero.NewOsFs(), dumps[0], path.Join(constants.BackupDir, latestStableDump)) | ||
if err != nil { | ||
return fmt.Errorf("unable to move dump to latest: %w", err) | ||
} | ||
|
||
err = os.Remove(dumps[0]) | ||
if err != nil { | ||
return fmt.Errorf("unable to clean up dump: %w", err) | ||
} | ||
|
||
db.log.Debugw("successfully took backup of meilisearch") | ||
|
||
if db.copyBinaryAfterBackup { | ||
// for a future upgrade, the current meilisearch binary is required | ||
err = db.copyMeilisearchBinary(ctx, true) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Check indicates whether a restore of the database is required or not. | ||
func (db *Meilisearch) Check(_ context.Context) (bool, error) { | ||
empty, err := utils.IsEmpty(db.datadir) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
if empty { | ||
db.log.Info("data directory is empty") | ||
return true, err | ||
} | ||
|
||
return false, nil | ||
} | ||
|
||
// Probe figures out if the database is running and available for taking backups. | ||
func (db *Meilisearch) Probe(_ context.Context) error { | ||
_, err := db.client.Version() | ||
if err != nil { | ||
return fmt.Errorf("connection error: %w", err) | ||
} | ||
|
||
healthy := db.client.IsHealthy() | ||
if !healthy { | ||
return fmt.Errorf("meilisearch does not report healthiness") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Recover restores a database backup | ||
func (db *Meilisearch) Recover(ctx context.Context) error { | ||
dump := path.Join(constants.RestoreDir, latestStableDump) | ||
|
||
if _, err := os.Stat(dump); os.IsNotExist(err) { | ||
return fmt.Errorf("restore file not present: %s", dump) | ||
} | ||
|
||
if err := utils.RemoveContents(db.datadir); err != nil { | ||
return fmt.Errorf("could not clean database data directory: %w", err) | ||
} | ||
|
||
start := time.Now() | ||
|
||
err := db.importDump(ctx, dump) | ||
if err != nil { | ||
return fmt.Errorf("unable to recover %w", err) | ||
} | ||
|
||
db.log.Infow("successfully restored meilisearch database", "duration", time.Since(start).String()) | ||
|
||
return nil | ||
} | ||
|
||
func (db *Meilisearch) importDump(ctx context.Context, dump string) error { | ||
var ( | ||
err error | ||
g, _ = errgroup.WithContext(ctx) | ||
|
||
handleFailedRecovery = func(restoreErr error) error { | ||
db.log.Errorw("trying to handle failed database recovery", "error", restoreErr) | ||
|
||
if err := os.RemoveAll(db.datadir); err != nil { | ||
db.log.Errorw("unable to cleanup database data directory after failed recovery attempt, high risk of starting with fresh database on container restart", "err", err) | ||
} else { | ||
db.log.Info("cleaned up database data directory after failed recovery attempt to prevent start of fresh database") | ||
} | ||
|
||
return restoreErr | ||
} | ||
) | ||
|
||
args := []string{"--import-dump", dump, "--master-key", db.apikey, "--dump-dir", constants.RestoreDir, "--db-path", db.datadir, "--http-addr", "localhost:1"} | ||
cmd := exec.CommandContext(ctx, meilisearchCmd, args...) | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
|
||
g.Go(func() error { | ||
db.log.Infow("execute meilisearch", "args", args) | ||
|
||
err = cmd.Run() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
db.log.Info("execution of meilisearch finished without an error") | ||
|
||
return nil | ||
}) | ||
|
||
restoreDB, err := New(db.log, db.datadir, "http://localhost:1", db.apikey) | ||
if err != nil { | ||
return fmt.Errorf("unable to create prober") | ||
} | ||
|
||
waitForRestore := func() error { | ||
ticker := time.NewTicker(3 * time.Second) | ||
defer ticker.Stop() | ||
sem := semaphore.NewWeighted(1) | ||
|
||
for { | ||
select { | ||
case <-ticker.C: | ||
if !sem.TryAcquire(1) { | ||
continue | ||
} | ||
|
||
err = restoreDB.Probe(ctx) | ||
sem.Release(1) | ||
if err != nil { | ||
db.log.Errorw("meilisearch is still restoring, continue probing for readiness...", "error", err) | ||
continue | ||
} | ||
|
||
db.log.Infow("meilisearch started after importing the dump, stopping it again for takeover from the database container") | ||
|
||
return nil | ||
case <-ctx.Done(): | ||
return fmt.Errorf("context cancelled during meilisearch restore") | ||
} | ||
} | ||
} | ||
|
||
if err := waitForRestore(); err != nil { | ||
return handleFailedRecovery(err) | ||
} | ||
|
||
if err := cmd.Process.Signal(syscall.SIGINT); err != nil { | ||
return handleFailedRecovery(err) | ||
} | ||
|
||
err = g.Wait() | ||
if err != nil { | ||
// will probably work better in meilisearch v1.4.0: https://github.com/meilisearch/meilisearch/commit/eff8570f591fe32a6106087807e3fe8c18e8e5e4 | ||
if strings.Contains(err.Error(), "interrupt") { | ||
db.log.Infow("meilisearch terminated but reported an error which can be ignored", "error", err) | ||
} else { | ||
return handleFailedRecovery(err) | ||
} | ||
} | ||
|
||
db.log.Info("successfully restored meilisearch database") | ||
|
||
return nil | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this too broad? Would 755 or something similar be enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably yes, but there are many other places where same permissions are used. Maybe we should create a separate PR and address this.