Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(71)

Side by Side Diff: build_scheduler/go/db/comments.go

Issue 2296763008: [task scheduler] Move files from build_scheduler/ to task_scheduler/ (Closed) Base URL: https://skia.googlesource.com/buildbot@master
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « build_scheduler/go/db/cache_test.go ('k') | build_scheduler/go/db/comments_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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(&copy); err != nil {
91 glog.Fatal(err)
92 }
93 return &copy
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 }
OLDNEW
« no previous file with comments | « build_scheduler/go/db/cache_test.go ('k') | build_scheduler/go/db/comments_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698