1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
14 "git.arvados.org/arvados.git/lib/controller/dblock"
15 "git.arvados.org/arvados.git/lib/ctrlctx"
16 "github.com/sirupsen/logrus"
19 type railsDatabase struct{}
21 func (runner railsDatabase) String() string {
22 return "railsDatabase"
25 // Run checks for and applies any pending Rails database migrations.
27 // If running a dev/test environment, and the database is empty, it
28 // initializes the database.
29 func (runner railsDatabase) Run(ctx context.Context, fail func(error), super *Supervisor) error {
30 err := super.wait(ctx, runPostgreSQL{}, installPassenger{src: "services/api"})
35 // determine path to installed rails app or source tree
37 if super.ClusterType == "production" {
38 appdir = "/var/lib/arvados/railsapi"
40 appdir = filepath.Join(super.SourcePath, "services/api")
43 // Check for pending migrations before running rake.
45 // In principle, we could use "rake db:migrate:status" or skip
46 // this check entirely and let "rake db:migrate" be a no-op
47 // most of the time. However, in the most common case when
48 // there are no new migrations, that would add ~2s to startup
49 // time / downtime during service restart.
51 todo, err := migrationList(appdir, super.logger)
56 // read schema_migrations table (list of migrations already
57 // applied) and remove those entries from todo
58 dbconnector := ctrlctx.DBConnector{PostgreSQL: super.cluster.PostgreSQL}
59 defer dbconnector.Close()
60 db, err := dbconnector.GetDB(ctx)
64 rows, err := db.QueryContext(ctx, `SELECT version FROM schema_migrations`)
66 if super.ClusterType == "production" {
69 super.logger.WithError(err).Info("schema_migrations query failed, trying db:setup")
70 return super.RunProgram(ctx, "services/api", runOptions{env: railsEnv}, "bundle", "exec", "rake", "db:setup")
85 // if nothing remains in todo, all available migrations are
86 // done, so return without running any [relatively slow]
92 super.logger.Infof("%d migrations pending", len(todo))
93 if !dblock.RailsMigrations.Lock(ctx, dbconnector.GetDB) {
94 return context.Canceled
96 defer dblock.RailsMigrations.Unlock()
97 return super.RunProgram(ctx, appdir, runOptions{env: railsEnv}, "bundle", "exec", "rake", "db:migrate")
100 func migrationList(dir string, log logrus.FieldLogger) (map[string]bool, error) {
101 todo := map[string]bool{}
103 // list versions in db/migrate/{version}_{name}.rb
104 err := fs.WalkDir(os.DirFS(dir), "db/migrate", func(path string, d fs.DirEntry, err error) error {
109 if strings.HasSuffix(fnm, "~") {
112 if !strings.HasSuffix(fnm, ".rb") {
113 log.Warnf("unexpected file in db/migrate dir: %s", fnm)
116 for i, c := range fnm {
117 if i > 0 && c == '_' {
121 if c < '0' || c > '9' {
122 // non-numeric character before the
123 // first '_' means this is not a
125 log.Warnf("unexpected file in db/migrate dir: %s", fnm)