OLD | NEW |
| (Empty) |
1 package db | |
2 | |
3 import ( | |
4 "bytes" | |
5 "encoding/gob" | |
6 "fmt" | |
7 "sync" | |
8 "time" | |
9 | |
10 "github.com/skia-dev/glog" | |
11 "go.skia.org/infra/go/util" | |
12 ) | |
13 | |
14 // TaskComment contains a comment about a Task. {Repo, Name, Commit, Timestamp} | |
15 // is used as the unique id for this comment. If TaskId is empty, the comment | |
16 // applies to all matching tasks. | |
17 type TaskComment struct { | |
18 Repo string `json:"repo"` | |
19 Name string `json:"name"` // Name of TaskSpec. | |
20 Commit string `json:"commit"` | |
21 // Timestamp is compared ignoring timezone. The timezone reflects User's | |
22 // location. | |
23 Timestamp time.Time `json:"time"` | |
24 TaskId string `json:"taskId"` | |
25 User string `json:"user"` | |
26 Message string `json:"message"` | |
27 } | |
28 | |
29 func (c TaskComment) Copy() *TaskComment { | |
30 return &c | |
31 } | |
32 | |
33 // TaskSpecComment contains a comment about a TaskSpec. {Repo, Name, Timestamp} | |
34 // is used as the unique id for this comment. | |
35 type TaskSpecComment struct { | |
36 Repo string `json:"repo"` | |
37 Name string `json:"name"` // Name of TaskSpec. | |
38 // Timestamp is compared ignoring timezone. The timezone reflects User's | |
39 // location. | |
40 Timestamp time.Time `json:"time"` | |
41 User string `json:"user"` | |
42 Flaky bool `json:"flaky"` | |
43 IgnoreFailure bool `json:"ignoreFailure"` | |
44 Message string `json:"message"` | |
45 } | |
46 | |
47 func (c TaskSpecComment) Copy() *TaskSpecComment { | |
48 return &c | |
49 } | |
50 | |
51 // CommitComment contains a comment about a commit. {Repo, Commit, Timestamp} is | |
52 // used as the unique id for this comment. | |
53 type CommitComment struct { | |
54 Repo string `json:"repo"` | |
55 Commit string `json:"commit"` | |
56 // Timestamp is compared ignoring timezone. The timezone reflects User's | |
57 // location. | |
58 Timestamp time.Time `json:"time"` | |
59 User string `json:"user"` | |
60 Message string `json:"message"` | |
61 } | |
62 | |
63 func (c CommitComment) Copy() *CommitComment { | |
64 return &c | |
65 } | |
66 | |
67 // RepoComments contains comments that all pertain to the same repository. | |
68 type RepoComments struct { | |
69 // Repo is the repository (Repo field) of all the comments contained in | |
70 // this RepoComments. | |
71 Repo string | |
72 // TaskComments maps TaskSpec name and commit hash to the comments for | |
73 // the matching Task, sorted by timestamp. | |
74 TaskComments map[string]map[string][]*TaskComment | |
75 // TaskSpecComments maps TaskSpec name to the comments for that | |
76 // TaskSpec, sorted by timestamp. | |
77 TaskSpecComments map[string][]*TaskSpecComment | |
78 // CommitComments maps commit hash to the comments for that commit, | |
79 // sorted by timestamp. | |
80 CommitComments map[string][]*CommitComment | |
81 } | |
82 | |
83 func (orig *RepoComments) Copy() *RepoComments { | |
84 // TODO(benjaminwagner): Make this more efficient. | |
85 b := bytes.Buffer{} | |
86 if err := gob.NewEncoder(&b).Encode(orig); err != nil { | |
87 glog.Fatal(err) | |
88 } | |
89 copy := RepoComments{} | |
90 if err := gob.NewDecoder(&b).Decode(©); err != nil { | |
91 glog.Fatal(err) | |
92 } | |
93 return © | |
94 } | |
95 | |
96 // CommentDB stores comments on Tasks, TaskSpecs, and commits. | |
97 // | |
98 // Clients must be tolerant of comments that refer to nonexistent Tasks, | |
99 // TaskSpecs, or commits. | |
100 type CommentDB interface { | |
101 // GetComments returns all comments for the given repos. | |
102 // | |
103 // If from is specified, it is a hint that TaskComments and CommitCommen
ts | |
104 // before this time will be ignored by the caller, thus they may be ommi
tted. | |
105 GetCommentsForRepos(repos []string, from time.Time) ([]*RepoComments, er
ror) | |
106 | |
107 // PutTaskComment inserts the TaskComment into the database. May return | |
108 // ErrAlreadyExists. | |
109 PutTaskComment(*TaskComment) error | |
110 | |
111 // DeleteTaskComment deletes the matching TaskComment from the database. | |
112 // Non-ID fields of the argument are ignored. | |
113 DeleteTaskComment(*TaskComment) error | |
114 | |
115 // PutTaskSpecComment inserts the TaskSpecComment into the database. May | |
116 // return ErrAlreadyExists. | |
117 PutTaskSpecComment(*TaskSpecComment) error | |
118 | |
119 // DeleteTaskSpecComment deletes the matching TaskSpecComment from the | |
120 // database. Non-ID fields of the argument are ignored. | |
121 DeleteTaskSpecComment(*TaskSpecComment) error | |
122 | |
123 // PutCommitComment inserts the CommitComment into the database. May ret
urn | |
124 // ErrAlreadyExists. | |
125 PutCommitComment(*CommitComment) error | |
126 | |
127 // DeleteCommitComment deletes the matching CommitComment from the datab
ase. | |
128 // Non-ID fields of the argument are ignored. | |
129 DeleteCommitComment(*CommitComment) error | |
130 } | |
131 | |
132 // CommentBox implements CommentDB with in-memory storage. | |
133 // | |
134 // When created via NewCommentBoxWithPersistence, CommentBox will persist the | |
135 // in-memory representation on every change using the provided writer function. | |
136 // | |
137 // CommentBox can be default-initialized if only in-memory storage is desired. | |
138 type CommentBox struct { | |
139 // mtx protects comments. | |
140 mtx sync.RWMutex | |
141 // comments is map[repo_name]*RepoComments. | |
142 comments map[string]*RepoComments | |
143 // writer is called to persist comments after every change. | |
144 writer func(map[string]*RepoComments) error | |
145 } | |
146 | |
147 // NewCommentBoxWithPersistence creates a CommentBox that is initialized with | |
148 // init and sends the updated in-memory representation to writer after each | |
149 // change. The value of init and the argument to writer is | |
150 // map[repo_name]*RepoComments. init must not be modified by the caller. writer | |
151 // must not call any methods of CommentBox. writer may return an error to | |
152 // prevent a change from taking effect. | |
153 func NewCommentBoxWithPersistence(init map[string]*RepoComments, writer func(map
[string]*RepoComments) error) *CommentBox { | |
154 return &CommentBox{ | |
155 comments: init, | |
156 writer: writer, | |
157 } | |
158 } | |
159 | |
160 // See documentation for CommentDB.GetCommentsForRepos. | |
161 func (b *CommentBox) GetCommentsForRepos(repos []string, from time.Time) ([]*Rep
oComments, error) { | |
162 b.mtx.RLock() | |
163 defer b.mtx.RUnlock() | |
164 rv := make([]*RepoComments, len(repos)) | |
165 for i, repo := range repos { | |
166 if rc, ok := b.comments[repo]; ok { | |
167 rv[i] = rc.Copy() | |
168 } else { | |
169 rv[i] = &RepoComments{Repo: repo} | |
170 } | |
171 } | |
172 return rv, nil | |
173 } | |
174 | |
175 // write calls b.writer with comments if non-null. | |
176 func (b *CommentBox) write() error { | |
177 if b.writer == nil { | |
178 return nil | |
179 } | |
180 return b.writer(b.comments) | |
181 } | |
182 | |
183 // getRepoComments returns the initialized *RepoComments for the given repo. | |
184 func (b *CommentBox) getRepoComments(repo string) *RepoComments { | |
185 if b.comments == nil { | |
186 b.comments = make(map[string]*RepoComments, 1) | |
187 } | |
188 rc, ok := b.comments[repo] | |
189 if !ok { | |
190 rc = &RepoComments{ | |
191 Repo: repo, | |
192 TaskComments: map[string]map[string][]*TaskComment{}
, | |
193 TaskSpecComments: map[string][]*TaskSpecComment{}, | |
194 CommitComments: map[string][]*CommitComment{}, | |
195 } | |
196 b.comments[repo] = rc | |
197 } | |
198 return rc | |
199 } | |
200 | |
201 // putTaskComment validates c and adds c to b.comments, or returns | |
202 // ErrAlreadyExists if a different comment has the same ID fields. Assumes b.mtx | |
203 // is write-locked. | |
204 func (b *CommentBox) putTaskComment(c *TaskComment) error { | |
205 if c.Repo == "" || c.Name == "" || c.Commit == "" || util.TimeIsZero(c.T
imestamp) { | |
206 return fmt.Errorf("TaskComment missing required fields. %#v", c) | |
207 } | |
208 rc := b.getRepoComments(c.Repo) | |
209 commitMap, ok := rc.TaskComments[c.Name] | |
210 if !ok { | |
211 commitMap = map[string][]*TaskComment{} | |
212 rc.TaskComments[c.Name] = commitMap | |
213 } | |
214 cSlice := commitMap[c.Commit] | |
215 // TODO(benjaminwagner): Would using utilities in the sort package make
this | |
216 // cleaner? | |
217 if len(cSlice) > 0 { | |
218 // Assume comments normally inserted at the end. | |
219 insert := 0 | |
220 for i := len(cSlice) - 1; i >= 0; i-- { | |
221 if cSlice[i].Timestamp.Equal(c.Timestamp) { | |
222 if *cSlice[i] == *c { | |
223 return nil | |
224 } else { | |
225 return ErrAlreadyExists | |
226 } | |
227 } else if cSlice[i].Timestamp.Before(c.Timestamp) { | |
228 insert = i + 1 | |
229 break | |
230 } | |
231 } | |
232 // Ensure capacity for another comment and move any comments aft
er the | |
233 // insertion point. | |
234 cSlice = append(cSlice, nil) | |
235 copy(cSlice[insert+1:], cSlice[insert:]) | |
236 cSlice[insert] = c.Copy() | |
237 } else { | |
238 cSlice = []*TaskComment{c.Copy()} | |
239 } | |
240 commitMap[c.Commit] = cSlice | |
241 return nil | |
242 } | |
243 | |
244 // deleteTaskComment validates c, then finds and removes a comment matching c's | |
245 // ID fields, returning the comment if found. Assumes b.mtx is write-locked. | |
246 func (b *CommentBox) deleteTaskComment(c *TaskComment) (*TaskComment, error) { | |
247 if c.Repo == "" || c.Name == "" || c.Commit == "" || util.TimeIsZero(c.T
imestamp) { | |
248 return nil, fmt.Errorf("TaskComment missing required fields. %#v
", c) | |
249 } | |
250 if rc, ok := b.comments[c.Repo]; ok { | |
251 if cSlice, ok := rc.TaskComments[c.Name][c.Commit]; ok { | |
252 // Assume linear search is fast. | |
253 for i, existing := range cSlice { | |
254 if existing.Timestamp.Equal(c.Timestamp) { | |
255 if len(cSlice) > 1 { | |
256 rc.TaskComments[c.Name][c.Commit
] = append(cSlice[:i], cSlice[i+1:]...) | |
257 } else { | |
258 delete(rc.TaskComments[c.Name],
c.Commit) | |
259 if len(rc.TaskComments[c.Name])
== 0 { | |
260 delete(rc.TaskComments,
c.Name) | |
261 } | |
262 } | |
263 return existing, nil | |
264 } | |
265 } | |
266 } | |
267 } | |
268 return nil, nil | |
269 } | |
270 | |
271 // See documentation for CommentDB.PutTaskComment. | |
272 func (b *CommentBox) PutTaskComment(c *TaskComment) error { | |
273 b.mtx.Lock() | |
274 defer b.mtx.Unlock() | |
275 if err := b.putTaskComment(c); err != nil { | |
276 return err | |
277 } | |
278 if err := b.write(); err != nil { | |
279 // If write returns an error, we must revert to previous. | |
280 if _, delErr := b.deleteTaskComment(c); delErr != nil { | |
281 glog.Warning("Unexpected error: %s", delErr) | |
282 } | |
283 return err | |
284 } | |
285 return nil | |
286 } | |
287 | |
288 // See documentation for CommentDB.DeleteTaskComment. | |
289 func (b *CommentBox) DeleteTaskComment(c *TaskComment) error { | |
290 b.mtx.Lock() | |
291 defer b.mtx.Unlock() | |
292 existing, err := b.deleteTaskComment(c) | |
293 if err != nil { | |
294 return err | |
295 } | |
296 if existing != nil { | |
297 if err := b.write(); err != nil { | |
298 // If write returns an error, we must revert to previous
. | |
299 if putErr := b.putTaskComment(existing); putErr != nil { | |
300 glog.Warning("Unexpected error: %s", putErr) | |
301 } | |
302 return err | |
303 } | |
304 } | |
305 return nil | |
306 } | |
307 | |
308 // putTaskSpecComment validates c and adds c to b.comments, or returns | |
309 // ErrAlreadyExists if a different comment has the same ID fields. Assumes b.mtx | |
310 // is write-locked. | |
311 func (b *CommentBox) putTaskSpecComment(c *TaskSpecComment) error { | |
312 if c.Repo == "" || c.Name == "" || util.TimeIsZero(c.Timestamp) { | |
313 return fmt.Errorf("TaskSpecComment missing required fields. %#v"
, c) | |
314 } | |
315 rc := b.getRepoComments(c.Repo) | |
316 cSlice := rc.TaskSpecComments[c.Name] | |
317 if len(cSlice) > 0 { | |
318 // Assume comments normally inserted at the end. | |
319 insert := 0 | |
320 for i := len(cSlice) - 1; i >= 0; i-- { | |
321 if cSlice[i].Timestamp.Equal(c.Timestamp) { | |
322 if *cSlice[i] == *c { | |
323 return nil | |
324 } else { | |
325 return ErrAlreadyExists | |
326 } | |
327 } else if cSlice[i].Timestamp.Before(c.Timestamp) { | |
328 insert = i + 1 | |
329 break | |
330 } | |
331 } | |
332 // Ensure capacity for another comment and move any comments aft
er the | |
333 // insertion point. | |
334 cSlice = append(cSlice, nil) | |
335 copy(cSlice[insert+1:], cSlice[insert:]) | |
336 cSlice[insert] = c.Copy() | |
337 } else { | |
338 cSlice = []*TaskSpecComment{c.Copy()} | |
339 } | |
340 rc.TaskSpecComments[c.Name] = cSlice | |
341 return nil | |
342 } | |
343 | |
344 // deleteTaskSpecComment validates c, then finds and removes a comment matching | |
345 // c's ID fields, returning the comment if found. Assumes b.mtx is write-locked. | |
346 func (b *CommentBox) deleteTaskSpecComment(c *TaskSpecComment) (*TaskSpecComment
, error) { | |
347 if c.Repo == "" || c.Name == "" || util.TimeIsZero(c.Timestamp) { | |
348 return nil, fmt.Errorf("TaskSpecComment missing required fields.
%#v", c) | |
349 } | |
350 if rc, ok := b.comments[c.Repo]; ok { | |
351 if cSlice, ok := rc.TaskSpecComments[c.Name]; ok { | |
352 // Assume linear search is fast. | |
353 for i, existing := range cSlice { | |
354 if existing.Timestamp.Equal(c.Timestamp) { | |
355 if len(cSlice) > 1 { | |
356 rc.TaskSpecComments[c.Name] = ap
pend(cSlice[:i], cSlice[i+1:]...) | |
357 } else { | |
358 delete(rc.TaskSpecComments, c.Na
me) | |
359 } | |
360 return existing, nil | |
361 } | |
362 } | |
363 } | |
364 } | |
365 return nil, nil | |
366 } | |
367 | |
368 // See documentation for CommentDB.PutTaskSpecComment. | |
369 func (b *CommentBox) PutTaskSpecComment(c *TaskSpecComment) error { | |
370 b.mtx.Lock() | |
371 defer b.mtx.Unlock() | |
372 if err := b.putTaskSpecComment(c); err != nil { | |
373 return err | |
374 } | |
375 if err := b.write(); err != nil { | |
376 // If write returns an error, we must revert to previous. | |
377 if _, delErr := b.deleteTaskSpecComment(c); delErr != nil { | |
378 glog.Warning("Unexpected error: %s", delErr) | |
379 } | |
380 return err | |
381 } | |
382 return nil | |
383 } | |
384 | |
385 // See documentation for CommentDB.DeleteTaskSpecComment. | |
386 func (b *CommentBox) DeleteTaskSpecComment(c *TaskSpecComment) error { | |
387 b.mtx.Lock() | |
388 defer b.mtx.Unlock() | |
389 existing, err := b.deleteTaskSpecComment(c) | |
390 if err != nil { | |
391 return err | |
392 } | |
393 if existing != nil { | |
394 if err := b.write(); err != nil { | |
395 // If write returns an error, we must revert to previous
. | |
396 if putErr := b.putTaskSpecComment(existing); putErr != n
il { | |
397 glog.Warning("Unexpected error: %s", putErr) | |
398 } | |
399 return err | |
400 } | |
401 } | |
402 return nil | |
403 } | |
404 | |
405 // putCommitComment validates c and adds c to b.comments, or returns | |
406 // ErrAlreadyExists if a different comment has the same ID fields. Assumes b.mtx | |
407 // is write-locked. | |
408 func (b *CommentBox) putCommitComment(c *CommitComment) error { | |
409 if c.Repo == "" || c.Commit == "" || util.TimeIsZero(c.Timestamp) { | |
410 return fmt.Errorf("CommitComment missing required fields. %#v",
c) | |
411 } | |
412 rc := b.getRepoComments(c.Repo) | |
413 cSlice := rc.CommitComments[c.Commit] | |
414 if len(cSlice) > 0 { | |
415 // Assume comments normally inserted at the end. | |
416 insert := 0 | |
417 for i := len(cSlice) - 1; i >= 0; i-- { | |
418 if cSlice[i].Timestamp.Equal(c.Timestamp) { | |
419 if *cSlice[i] == *c { | |
420 return nil | |
421 } else { | |
422 return ErrAlreadyExists | |
423 } | |
424 } else if cSlice[i].Timestamp.Before(c.Timestamp) { | |
425 insert = i + 1 | |
426 break | |
427 } | |
428 } | |
429 // Ensure capacity for another comment and move any comments aft
er the | |
430 // insertion point. | |
431 cSlice = append(cSlice, nil) | |
432 copy(cSlice[insert+1:], cSlice[insert:]) | |
433 cSlice[insert] = c.Copy() | |
434 } else { | |
435 cSlice = []*CommitComment{c.Copy()} | |
436 } | |
437 rc.CommitComments[c.Commit] = cSlice | |
438 return nil | |
439 } | |
440 | |
441 // deleteCommitComment validates c, then finds and removes a comment matching | |
442 // c's ID fields, returning the comment if found. Assumes b.mtx is write-locked. | |
443 func (b *CommentBox) deleteCommitComment(c *CommitComment) (*CommitComment, erro
r) { | |
444 if c.Repo == "" || c.Commit == "" || util.TimeIsZero(c.Timestamp) { | |
445 return nil, fmt.Errorf("CommitComment missing required fields. %
#v", c) | |
446 } | |
447 if rc, ok := b.comments[c.Repo]; ok { | |
448 if cSlice, ok := rc.CommitComments[c.Commit]; ok { | |
449 // Assume linear search is fast. | |
450 for i, existing := range cSlice { | |
451 if existing.Timestamp.Equal(c.Timestamp) { | |
452 if len(cSlice) > 1 { | |
453 rc.CommitComments[c.Commit] = ap
pend(cSlice[:i], cSlice[i+1:]...) | |
454 } else { | |
455 delete(rc.CommitComments, c.Comm
it) | |
456 } | |
457 return existing, nil | |
458 } | |
459 } | |
460 } | |
461 } | |
462 return nil, nil | |
463 } | |
464 | |
465 // See documentation for CommentDB.PutCommitComment. | |
466 func (b *CommentBox) PutCommitComment(c *CommitComment) error { | |
467 b.mtx.Lock() | |
468 defer b.mtx.Unlock() | |
469 if err := b.putCommitComment(c); err != nil { | |
470 return err | |
471 } | |
472 if err := b.write(); err != nil { | |
473 // If write returns an error, we must revert to previous. | |
474 if _, delErr := b.deleteCommitComment(c); delErr != nil { | |
475 glog.Warning("Unexpected error: %s", delErr) | |
476 } | |
477 return err | |
478 } | |
479 return nil | |
480 } | |
481 | |
482 // See documentation for CommentDB.DeleteCommitComment. | |
483 func (b *CommentBox) DeleteCommitComment(c *CommitComment) error { | |
484 b.mtx.Lock() | |
485 defer b.mtx.Unlock() | |
486 existing, err := b.deleteCommitComment(c) | |
487 if err != nil { | |
488 return err | |
489 } | |
490 if existing != nil { | |
491 if err := b.write(); err != nil { | |
492 // If write returns an error, we must revert to previous
. | |
493 if putErr := b.putCommitComment(existing); putErr != nil
{ | |
494 glog.Warning("Unexpected error: %s", putErr) | |
495 } | |
496 return err | |
497 } | |
498 } | |
499 return nil | |
500 } | |
OLD | NEW |