Browse Source

implement database auto-upgrades (#243)

tags/v0.12.0
Shivaram Lingamneni 6 years ago
parent
commit
69fd3ac324
5 changed files with 130 additions and 31 deletions
  1. 2
    1
      irc/config.go
  2. 115
    22
      irc/database.go
  3. 5
    7
      irc/server.go
  4. 4
    1
      oragono.go
  5. 4
    0
      oragono.yaml

+ 2
- 1
irc/config.go View File

@@ -229,7 +229,8 @@ type Config struct {
229 229
 	}
230 230
 
231 231
 	Datastore struct {
232
-		Path string
232
+		Path        string
233
+		AutoUpgrade *bool
233 234
 	}
234 235
 
235 236
 	Accounts AccountConfig

+ 115
- 22
irc/database.go View File

@@ -8,9 +8,11 @@ import (
8 8
 	"encoding/base64"
9 9
 	"encoding/json"
10 10
 	"fmt"
11
+	"io"
11 12
 	"log"
12 13
 	"os"
13 14
 	"strings"
15
+	"time"
14 16
 
15 17
 	"github.com/oragono/oragono/irc/modes"
16 18
 	"github.com/oragono/oragono/irc/passwd"
@@ -38,6 +40,22 @@ type SchemaChange struct {
38 40
 // maps an initial version to a schema change capable of upgrading it
39 41
 var schemaChanges map[string]SchemaChange
40 42
 
43
+type incompatibleSchemaError struct {
44
+	currentVersion  string
45
+	requiredVersion string
46
+}
47
+
48
+func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
49
+	return &incompatibleSchemaError{
50
+		currentVersion:  currentVersion,
51
+		requiredVersion: latestDbSchema,
52
+	}
53
+}
54
+
55
+func (err *incompatibleSchemaError) Error() string {
56
+	return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
57
+}
58
+
41 59
 // InitDB creates the database.
42 60
 func InitDB(path string) {
43 61
 	// prepare kvstore db
@@ -69,36 +87,107 @@ func InitDB(path string) {
69 87
 }
70 88
 
71 89
 // OpenDatabase returns an existing database, performing a schema version check.
72
-func OpenDatabase(path string) (*buntdb.DB, error) {
73
-	// open data store
74
-	db, err := buntdb.Open(path)
90
+func OpenDatabase(config *Config) (*buntdb.DB, error) {
91
+	allowAutoupgrade := true
92
+	if config.Datastore.AutoUpgrade != nil {
93
+		allowAutoupgrade = *config.Datastore.AutoUpgrade
94
+	}
95
+	return openDatabaseInternal(config, allowAutoupgrade)
96
+}
97
+
98
+// open the database, giving it at most one chance to auto-upgrade the schema
99
+func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
100
+	db, err = buntdb.Open(config.Datastore.Path)
75 101
 	if err != nil {
76
-		return nil, err
102
+		return
77 103
 	}
78 104
 
79
-	// check db version
80
-	err = db.View(func(tx *buntdb.Tx) error {
81
-		version, _ := tx.Get(keySchemaVersion)
82
-		if version != latestDbSchema {
83
-			return fmt.Errorf("Database must be updated. Expected schema v%s, got v%s", latestDbSchema, version)
105
+	defer func() {
106
+		if err != nil && db != nil {
107
+			db.Close()
108
+			db = nil
84 109
 		}
85
-		return nil
110
+	}()
111
+
112
+	// read the current version string
113
+	var version string
114
+	err = db.View(func(tx *buntdb.Tx) error {
115
+		version, err = tx.Get(keySchemaVersion)
116
+		return err
86 117
 	})
118
+	if err != nil {
119
+		return
120
+	}
87 121
 
122
+	if version == latestDbSchema {
123
+		// success
124
+		return
125
+	}
126
+
127
+	// XXX quiesce the DB so we can be sure it's safe to make a backup copy
128
+	db.Close()
129
+	db = nil
130
+	if allowAutoupgrade {
131
+		err = performAutoUpgrade(version, config)
132
+		if err != nil {
133
+			return
134
+		}
135
+		// successful autoupgrade, let's try this again:
136
+		return openDatabaseInternal(config, false)
137
+	} else {
138
+		err = IncompatibleSchemaError(version)
139
+		return
140
+	}
141
+}
142
+
143
+// implementation of `cp` (go should really provide this...)
144
+func cpFile(src string, dst string) (err error) {
145
+	in, err := os.Open(src)
146
+	if err != nil {
147
+		return
148
+	}
149
+	defer in.Close()
150
+	out, err := os.Create(dst)
88 151
 	if err != nil {
89
-		// close the db
90
-		db.Close()
91
-		return nil, err
152
+		return
153
+	}
154
+	defer func() {
155
+		closeError := out.Close()
156
+		if err == nil {
157
+			err = closeError
158
+		}
159
+	}()
160
+	if _, err = io.Copy(out, in); err != nil {
161
+		return
92 162
 	}
163
+	return
164
+}
93 165
 
94
-	return db, nil
166
+func performAutoUpgrade(currentVersion string, config *Config) (err error) {
167
+	path := config.Datastore.Path
168
+	log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
169
+	timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
170
+	backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
171
+	log.Printf("making a backup of current database at %s\n", backupPath)
172
+	err = cpFile(path, backupPath)
173
+	if err != nil {
174
+		return err
175
+	}
176
+
177
+	err = UpgradeDB(config)
178
+	if err != nil {
179
+		// database upgrade is a single transaction, so we don't need to restore the backup;
180
+		// we can just delete it
181
+		os.Remove(backupPath)
182
+	}
183
+	return err
95 184
 }
96 185
 
97 186
 // UpgradeDB upgrades the datastore to the latest schema.
98
-func UpgradeDB(config *Config) {
187
+func UpgradeDB(config *Config) (err error) {
99 188
 	store, err := buntdb.Open(config.Datastore.Path)
100 189
 	if err != nil {
101
-		log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
190
+		return err
102 191
 	}
103 192
 	defer store.Close()
104 193
 
@@ -108,9 +197,14 @@ func UpgradeDB(config *Config) {
108 197
 			version, _ = tx.Get(keySchemaVersion)
109 198
 			change, schemaNeedsChange := schemaChanges[version]
110 199
 			if !schemaNeedsChange {
111
-				break
200
+				if version == latestDbSchema {
201
+					// success!
202
+					break
203
+				}
204
+				// unable to upgrade to the desired version, roll back
205
+				return IncompatibleSchemaError(version)
112 206
 			}
113
-			log.Println("attempting to update store from version " + version)
207
+			log.Println("attempting to update schema from version " + version)
114 208
 			err := change.Changer(config, tx)
115 209
 			if err != nil {
116 210
 				return err
@@ -119,16 +213,15 @@ func UpgradeDB(config *Config) {
119 213
 			if err != nil {
120 214
 				return err
121 215
 			}
122
-			log.Println("successfully updated store to version " + change.TargetVersion)
216
+			log.Println("successfully updated schema to version " + change.TargetVersion)
123 217
 		}
124 218
 		return nil
125 219
 	})
126 220
 
127 221
 	if err != nil {
128
-		log.Fatal("Could not update datastore:", err.Error())
222
+		log.Println("database upgrade failed and was rolled back")
129 223
 	}
130
-
131
-	return
224
+	return err
132 225
 }
133 226
 
134 227
 func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {

+ 5
- 7
irc/server.go View File

@@ -127,7 +127,6 @@ type Server struct {
127 127
 	signals                    chan os.Signal
128 128
 	snomasks                   *SnoManager
129 129
 	store                      *buntdb.DB
130
-	storeFilename              string
131 130
 	stsEnabled                 bool
132 131
 	webirc                     []webircConfig
133 132
 	whoWas                     *WhoWasList
@@ -746,7 +745,7 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
746 745
 			return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
747 746
 		} else if server.name != config.Server.Name {
748 747
 			return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
749
-		} else if server.storeFilename != config.Datastore.Path {
748
+		} else if server.config.Datastore.Path != config.Datastore.Path {
750 749
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
751 750
 		}
752 751
 	}
@@ -966,10 +965,9 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
966 965
 	server.config = config
967 966
 	server.configurableStateMutex.Unlock()
968 967
 
969
-	server.storeFilename = config.Datastore.Path
970
-	server.logger.Info("rehash", "Using datastore", server.storeFilename)
968
+	server.logger.Info("rehash", "Using datastore", config.Datastore.Path)
971 969
 	if initial {
972
-		if err := server.loadDatastore(server.storeFilename); err != nil {
970
+		if err := server.loadDatastore(config); err != nil {
973 971
 			return err
974 972
 		}
975 973
 	}
@@ -1066,11 +1064,11 @@ func (server *Server) loadMOTD(motdPath string, useFormatting bool) error {
1066 1064
 	return nil
1067 1065
 }
1068 1066
 
1069
-func (server *Server) loadDatastore(datastorePath string) error {
1067
+func (server *Server) loadDatastore(config *Config) error {
1070 1068
 	// open the datastore and load server state for which it (rather than config)
1071 1069
 	// is the source of truth
1072 1070
 
1073
-	db, err := OpenDatabase(datastorePath)
1071
+	db, err := OpenDatabase(config)
1074 1072
 	if err == nil {
1075 1073
 		server.store = db
1076 1074
 	} else {

+ 4
- 1
oragono.go View File

@@ -84,7 +84,10 @@ Options:
84 84
 			log.Println("database initialized: ", config.Datastore.Path)
85 85
 		}
86 86
 	} else if arguments["upgradedb"].(bool) {
87
-		irc.UpgradeDB(config)
87
+		err = irc.UpgradeDB(config)
88
+		if err != nil {
89
+			log.Fatal("Error while upgrading db:", err.Error())
90
+		}
88 91
 		if !arguments["--quiet"].(bool) {
89 92
 			log.Println("database upgraded: ", config.Datastore.Path)
90 93
 		}

+ 4
- 0
oragono.yaml View File

@@ -341,6 +341,10 @@ debug:
341 341
 datastore:
342 342
     # path to the datastore
343 343
     path: ircd.db
344
+    # if the database schema requires an upgrade, `autoupgrade` (which defaults to true)
345
+    # will attempt to perform it automatically on startup. the database will be backed
346
+    # up, and if the upgrade fails, the original database will be restored.
347
+    autoupgrade: true
344 348
 
345 349
 # languages config
346 350
 languages:

Loading…
Cancel
Save