forked from juju/juju
/
schema.go
194 lines (172 loc) · 5.85 KB
/
schema.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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package lease
import (
"fmt"
"strings"
"time"
"github.com/juju/errors"
"github.com/juju/juju/core/lease"
)
// These constants define the field names and type values used by documents in
// a lease collection. They *must* remain in sync with the bson marshalling
// annotations in leaseDoc and clockDoc.
const (
// fieldType and fieldNamespace identify the Type and Namespace fields in
// both leaseDoc and clockDoc.
fieldType = "type"
fieldNamespace = "namespace"
// typeLease and typeClock are the acceptable values for fieldType.
typeLease = "lease"
typeClock = "clock"
// fieldLease* identify the fields in a leaseDoc.
fieldLeaseHolder = "holder"
fieldLeaseExpiry = "expiry"
fieldLeaseWriter = "writer"
// fieldClock* identify the fields in a clockDoc.
fieldClockWriters = "writers"
)
// toInt64 converts a local time.Time into a database value that doesn't
// silently lose precision.
func toInt64(t time.Time) int64 {
return t.UnixNano()
}
// toTime converts a toInt64 result, as loaded from the db, back to a time.Time.
func toTime(v int64) time.Time {
return time.Unix(0, v)
}
// leaseDocId returns the _id for the document holding details of the supplied
// namespace and lease.
func leaseDocId(namespace, lease string) string {
return fmt.Sprintf("%s#%s#%s#", typeLease, namespace, lease)
}
// leaseDoc is used to serialise lease entries.
type leaseDoc struct {
// Id is always "<Type>#<Namespace>#<Name>#", and <Type> is always "lease",
// so that we can extract useful information from a stream of watcher events
// without incurring extra DB hits.
// Apart from checking validity on load, though, there's little reason
// to use Id elsewhere; Namespace and Name are the sources of truth.
Id string `bson:"_id"`
Type string `bson:"type"`
Namespace string `bson:"namespace"`
Name string `bson:"name"`
// Holder, Expiry, and Writer map directly to entry.
Holder string `bson:"holder"`
Expiry int64 `bson:"expiry"`
Writer string `bson:"writer"`
}
// validate returns an error if any fields are invalid or inconsistent.
func (doc leaseDoc) validate() error {
if doc.Type != typeLease {
return errors.Errorf("invalid type %q", doc.Type)
}
// state.multiModelRunner prepends environ ids in our documents, and
// state.modelStateCollection does not strip them out.
if !strings.HasSuffix(doc.Id, leaseDocId(doc.Namespace, doc.Name)) {
return errors.Errorf("inconsistent _id")
}
if err := lease.ValidateString(doc.Holder); err != nil {
return errors.Annotatef(err, "invalid holder")
}
if doc.Expiry == 0 {
return errors.Errorf("invalid expiry")
}
if err := lease.ValidateString(doc.Writer); err != nil {
return errors.Annotatef(err, "invalid writer")
}
return nil
}
// entry returns the lease name and an entry corresponding to the document. If
// the document cannot be validated, it returns an error.
func (doc leaseDoc) entry() (string, entry, error) {
if err := doc.validate(); err != nil {
return "", entry{}, errors.Trace(err)
}
entry := entry{
holder: doc.Holder,
expiry: toTime(doc.Expiry),
writer: doc.Writer,
}
return doc.Name, entry, nil
}
// newLeaseDoc returns a valid lease document encoding the supplied lease and
// entry in the supplied namespace, or an error.
func newLeaseDoc(namespace, name string, entry entry) (*leaseDoc, error) {
doc := &leaseDoc{
Id: leaseDocId(namespace, name),
Type: typeLease,
Namespace: namespace,
Name: name,
Holder: entry.holder,
Expiry: toInt64(entry.expiry),
Writer: entry.writer,
}
if err := doc.validate(); err != nil {
return nil, errors.Trace(err)
}
return doc, nil
}
// clockDocId returns the _id for the document holding clock skew information
// for clients that have written in the supplied namespace.
func clockDocId(namespace string) string {
return fmt.Sprintf("%s#%s#", typeClock, namespace)
}
// clockDoc is used to synchronise clients.
type clockDoc struct {
// Id is always "<Type>#<Namespace>#", and <Type> is always "clock", for
// consistency with leaseDoc and ease of querying within the collection.
Id string `bson:"_id"`
Type string `bson:"type"`
Namespace string `bson:"namespace"`
// Writers holds the latest acknowledged time for every known client.
Writers map[string]int64 `bson:"writers"`
}
// validate returns an error if any fields are invalid or inconsistent.
func (doc clockDoc) validate() error {
if doc.Type != typeClock {
return errors.Errorf("invalid type %q", doc.Type)
}
// state.multiModelRunner prepends environ ids in our documents, and
// state.modelStateCollection does not strip them out.
if !strings.HasSuffix(doc.Id, clockDocId(doc.Namespace)) {
return errors.Errorf("inconsistent _id")
}
for writer, written := range doc.Writers {
if written == 0 {
return errors.Errorf("invalid time for writer %q", writer)
}
}
return nil
}
// skews returns clock skew information for all writers recorded in the
// document, given that the document was read between the supplied local
// times. It will return an error if the clock document is not valid, or
// if the times don't make sense.
func (doc clockDoc) skews(beginning, end time.Time) (map[string]Skew, error) {
if err := doc.validate(); err != nil {
return nil, errors.Trace(err)
}
skews := make(map[string]Skew)
for writer, written := range doc.Writers {
skews[writer] = Skew{
LastWrite: toTime(written),
Beginning: beginning,
End: end,
}
}
return skews, nil
}
// newClockDoc returns an empty clockDoc for the supplied namespace.
func newClockDoc(namespace string) (*clockDoc, error) {
doc := &clockDoc{
Id: clockDocId(namespace),
Type: typeClock,
Namespace: namespace,
Writers: make(map[string]int64),
}
if err := doc.validate(); err != nil {
return nil, errors.Trace(err)
}
return doc, nil
}