-
Notifications
You must be signed in to change notification settings - Fork 9
/
registry.go
168 lines (142 loc) · 3.31 KB
/
registry.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package database
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"sync"
"time"
"github.com/tevino/abool"
)
const (
registryFileName = "databases.json"
)
var (
registryPersistence = abool.NewBool(false)
writeRegistrySoon = abool.NewBool(false)
registry = make(map[string]*Database)
registryLock sync.Mutex
nameConstraint = regexp.MustCompile("^[A-Za-z0-9_-]{3,}$")
)
// Register registers a new database.
// If the database is already registered, only
// the description and the primary API will be
// updated and the effective object will be returned.
func Register(db *Database) (*Database, error) {
if !initialized.IsSet() {
return nil, errors.New("database not initialized")
}
registryLock.Lock()
defer registryLock.Unlock()
registeredDB, ok := registry[db.Name]
save := false
if ok {
// update database
if registeredDB.Description != db.Description {
registeredDB.Description = db.Description
save = true
}
if registeredDB.ShadowDelete != db.ShadowDelete {
registeredDB.ShadowDelete = db.ShadowDelete
save = true
}
} else {
// register new database
if !nameConstraint.MatchString(db.Name) {
return nil, errors.New("database name must only contain alphanumeric and `_-` characters and must be at least 3 characters long")
}
now := time.Now().Round(time.Second)
db.Registered = now
db.LastUpdated = now
db.LastLoaded = time.Time{}
registry[db.Name] = db
save = true
}
if save && registryPersistence.IsSet() {
if ok {
registeredDB.Updated()
}
err := saveRegistry(false)
if err != nil {
return nil, err
}
}
if ok {
return registeredDB, nil
}
return nil, nil
}
func getDatabase(name string) (*Database, error) {
registryLock.Lock()
defer registryLock.Unlock()
registeredDB, ok := registry[name]
if !ok {
return nil, fmt.Errorf(`database "%s" not registered`, name)
}
if time.Now().Add(-24 * time.Hour).After(registeredDB.LastLoaded) {
writeRegistrySoon.Set()
}
registeredDB.Loaded()
return registeredDB, nil
}
// EnableRegistryPersistence enables persistence of the database registry.
func EnableRegistryPersistence() {
if registryPersistence.SetToIf(false, true) {
// start registry writer
go registryWriter()
// TODO: make an initial write if database system is already initialized
}
}
func loadRegistry() error {
registryLock.Lock()
defer registryLock.Unlock()
// read file
filePath := path.Join(rootStructure.Path, registryFileName)
data, err := ioutil.ReadFile(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
// parse
databases := make(map[string]*Database)
err = json.Unmarshal(data, &databases)
if err != nil {
return err
}
// set
registry = databases
return nil
}
func saveRegistry(lock bool) error {
if lock {
registryLock.Lock()
defer registryLock.Unlock()
}
// marshal
data, err := json.MarshalIndent(registry, "", "\t")
if err != nil {
return err
}
// write file
// TODO: write atomically (best effort)
filePath := path.Join(rootStructure.Path, registryFileName)
return ioutil.WriteFile(filePath, data, 0o0600)
}
func registryWriter() {
for {
select {
case <-time.After(1 * time.Hour):
if writeRegistrySoon.SetToIf(true, false) {
_ = saveRegistry(true)
}
case <-shutdownSignal:
_ = saveRegistry(true)
return
}
}
}