Skip to content

Commit 9db0a6d

Browse files
committed
Make db waiter to wait latest migration
1 parent 667bd2f commit 9db0a6d

File tree

7 files changed

+119
-50
lines changed

7 files changed

+119
-50
lines changed

components/gitpod-db/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"license": "AGPL-3.0",
44
"version": "0.1.5",
55
"scripts": {
6-
"build": "yarn lint && npx tsc",
6+
"build": "yarn lint && npx tsc && yarn generate-latest-migration",
77
"build:clean": "yarn clean && yarn build",
88
"lint": "yarn eslint src/*.ts src/**/*.ts",
99
"lint:fix": "yarn eslint src/*.ts src/**/*.ts --fix",
@@ -18,7 +18,8 @@
1818
"migrate-migrations": "node ./lib/migrate-migrations.js",
1919
"clean": "yarn run rimraf lib",
2020
"clean:node": "yarn run rimraf node_modules",
21-
"purge": "yarn clean && yarn clean:node && yarn run rimraf yarn.lock"
21+
"purge": "yarn clean && yarn clean:node && yarn run rimraf yarn.lock",
22+
"generate-latest-migration": "node lib/typeorm/generate-latest-migration.js"
2223
},
2324
"files": [
2425
"/lib"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { MigrationExecutor } from "typeorm";
8+
import { writeFile } from "node:fs/promises";
9+
import { Config } from "../config";
10+
import { TypeORM } from "./typeorm";
11+
import { join } from "node:path";
12+
13+
async function generateLatestMigrationName() {
14+
const config = new Config();
15+
const typeorm = new TypeORM(config, {});
16+
const conn = await typeorm.getConnection();
17+
const t = await new MigrationExecutor(conn).getAllMigrations();
18+
await writeFile(join(__dirname, "latest-migration.txt"), t.sort((m) => m.timestamp).pop()?.name ?? "");
19+
process.exit(0);
20+
}
21+
22+
generateLatestMigrationName();

components/service-waiter/BUILD.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ packages:
77
- "go.sum"
88
deps:
99
- components/common-go:lib
10+
- components/gitpod-db:lib
11+
prep:
12+
- ["sh", "-c", "mkdir -p cmd/resources"]
13+
- ["sh", "-c", "cat _deps/components-gitpod-db--lib/package/lib/typeorm/latest-migration.txt > cmd/resources/latest-migration.txt"]
1014
env:
1115
- CGO_ENABLED=0
1216
- GOOS=linux

components/service-waiter/cmd/database.go

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
package cmd
66

77
import (
8+
"context"
89
"crypto/tls"
910
"crypto/x509"
1011
"database/sql"
12+
_ "embed"
13+
"errors"
1114
"net"
1215
"os"
1316
"strings"
@@ -20,6 +23,11 @@ import (
2023
"github.com/gitpod-io/gitpod/common-go/log"
2124
)
2225

26+
const migrationTableName = "migrations"
27+
28+
//go:embed resources/latest-migration.txt
29+
var latestMigrationName string
30+
2331
// databaseCmd represents the database command
2432
var databaseCmd = &cobra.Command{
2533
Use: "database",
@@ -34,11 +42,19 @@ DB_CA_CERT and DB_USER(=gitpod)`,
3442
}
3543
},
3644
Run: func(cmd *cobra.Command, args []string) {
45+
timeout := getTimeout()
46+
ctx, cancel := context.WithTimeout(cmd.Context(), timeout)
47+
defer cancel()
48+
3749
cfg := mysql.NewConfig()
3850
cfg.Addr = net.JoinHostPort(viper.GetString("host"), viper.GetString("port"))
3951
cfg.Net = "tcp"
4052
cfg.User = viper.GetString("username")
4153
cfg.Passwd = viper.GetString("password")
54+
55+
// Must be "gitpod"
56+
// Align to https://github.com/gitpod-io/gitpod/blob/884d922e8e33d8b936ec18d7fe3c8dcffde42b5a/components/gitpod-db/go/conn.go#L37
57+
cfg.DBName = "gitpod"
4258
cfg.Timeout = 1 * time.Second
4359

4460
dsn := cfg.FormatDSN()
@@ -65,46 +81,59 @@ DB_CA_CERT and DB_USER(=gitpod)`,
6581
cfg.TLSConfig = tlsConfigName
6682
}
6783

68-
timeout := getTimeout()
69-
done := make(chan bool)
70-
go func() {
71-
log.WithField("timeout", timeout.String()).WithField("dsn", censoredDSN).Info("attempting to connect to DB")
72-
for {
73-
db, err := sql.Open("mysql", cfg.FormatDSN())
74-
if err != nil {
75-
continue
76-
}
77-
err = db.Ping()
78-
if err != nil {
79-
if strings.Contains(err.Error(), "Access denied") {
80-
fail("Invalid credentials for the database. Check DB_USERNAME and DB_PASSWORD.")
81-
}
82-
83-
log.WithError(err).Debug("retry")
84-
<-time.After(time.Second)
85-
continue
86-
}
87-
88-
err = db.Close()
89-
if err != nil {
90-
log.WithError(err).Warn("cannot close DB connection used for probing")
91-
}
84+
migrationName := GetLatestMigrationName()
85+
log.WithField("timeout", timeout.String()).WithField("dsn", censoredDSN).WithField("migration", migrationName).Info("waiting for database")
86+
for ctx.Err() == nil {
87+
log.Info("attempting to check if database is available")
88+
if err := checkDbAvailable(ctx, cfg, migrationName); err != nil {
89+
log.WithError(err).Debug("retry")
90+
<-time.After(time.Second)
91+
} else {
9292
break
9393
}
94+
}
9495

95-
done <- true
96-
}()
97-
98-
select {
99-
case <-done:
96+
if ctx.Err() != nil {
97+
log.WithField("timeout", timeout.String()).WithError(ctx.Err()).Fatal("database did not become available in time")
98+
} else {
10099
log.Info("database became available")
101-
return
102-
case <-time.After(timeout):
103-
log.WithField("timeout", timeout.String()).Fatal("database did not become available in time")
104100
}
105101
},
106102
}
107103

104+
func GetLatestMigrationName() string {
105+
return strings.TrimSpace(latestMigrationName)
106+
}
107+
108+
// checkDbAvailable will connect and check if migrations table contains the latest migration
109+
func checkDbAvailable(ctx context.Context, cfg *mysql.Config, migration string) error {
110+
db, err := sql.Open("mysql", cfg.FormatDSN())
111+
if err != nil {
112+
return err
113+
}
114+
// ignore error
115+
defer db.Close()
116+
117+
// if migration name is not set, just ping the database
118+
if migration == "" {
119+
return db.PingContext(ctx)
120+
}
121+
122+
log.Info("checking if database is migrated")
123+
row := db.QueryRowContext(ctx, "SELECT name FROM "+migrationTableName+" WHERE name = ?", migration)
124+
var name string
125+
if err := row.Scan(&name); err != nil {
126+
if errors.Is(err, sql.ErrNoRows) {
127+
log.Error("not yet migrated")
128+
return err
129+
}
130+
log.WithError(err).Error("failed to query migrations")
131+
return err
132+
}
133+
134+
return nil
135+
}
136+
108137
func init() {
109138
rootCmd.AddCommand(databaseCmd)
110139

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package cmd
6+
7+
import (
8+
_ "embed"
9+
"os"
10+
"strings"
11+
"testing"
12+
)
13+
14+
func TestGetLatestMigrationName(t *testing.T) {
15+
t.Run("should have latest migration name", func(t *testing.T) {
16+
path, err := os.Getwd()
17+
if err != nil {
18+
t.Errorf("failed to get current work dir: %v", err)
19+
return
20+
}
21+
if !strings.Contains(path, "components-service-waiter--app") {
22+
t.Skip("skipping test; not running in leeway build")
23+
return
24+
}
25+
if GetLatestMigrationName() == "" {
26+
t.Errorf("migration name should not be empty")
27+
}
28+
})
29+
}

components/service-waiter/cmd/resources/latest-migration.txt

Whitespace-only changes.

components/service-waiter/cmd/root.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,6 @@ func getTimeout() time.Duration {
100100
return timeout
101101
}
102102

103-
// fail ends the waiting process propagating the message on its way out
104-
func fail(message string) {
105-
terminationLog := "/dev/termination-log"
106-
107-
log.WithField("message", message).Warn("failed to wait for a service")
108-
109-
if _, err := os.Stat(terminationLog); !os.IsNotExist(err) {
110-
err := os.WriteFile(terminationLog, []byte(message), 0600)
111-
if err != nil {
112-
log.WithError(err).Error("cannot write termination log")
113-
}
114-
}
115-
116-
os.Exit(1)
117-
}
118-
119103
func envOrDefault(env, def string) (res string) {
120104
res = os.Getenv(env)
121105
if res == "" {

0 commit comments

Comments
 (0)