| package recent_rolls |
| |
| import ( |
| "bytes" |
| "encoding/binary" |
| "encoding/json" |
| "fmt" |
| "time" |
| |
| "github.com/boltdb/bolt" |
| "go.skia.org/infra/go/autoroll" |
| "go.skia.org/infra/go/sklog" |
| "go.skia.org/infra/go/util" |
| ) |
| |
| var ( |
| BUCKET_ROLLS = []byte("rolls") |
| BUCKET_ROLLS_BY_DATE = []byte("rollsByDate") |
| ) |
| |
| // db is a struct used for interacting with a database. |
| type db struct { |
| db *bolt.DB |
| } |
| |
| // openDB returns a db instance. |
| func openDB(filename string) (*db, error) { |
| d, err := bolt.Open(filename, 0600, nil) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := d.Update(func(tx *bolt.Tx) error { |
| if _, err := tx.CreateBucketIfNotExists(BUCKET_ROLLS); err != nil { |
| return err |
| } |
| if _, err := tx.CreateBucketIfNotExists(BUCKET_ROLLS_BY_DATE); err != nil { |
| return err |
| } |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| |
| return &db{d}, nil |
| } |
| |
| // Close closes the db. |
| func (d *db) Close() error { |
| return d.db.Close() |
| } |
| |
| // issueToRollKey converts an issue number to a BoltDB key. |
| func issueToRollKey(issue int64) []byte { |
| var buf bytes.Buffer |
| if err := binary.Write(&buf, binary.LittleEndian, issue); err != nil { |
| sklog.Fatalf("Failed to serialize int64: %d", issue) |
| } |
| return buf.Bytes() |
| } |
| |
| // rollKey returns a BoltDB key for the given AutoRollIssue based on its issue |
| // number. |
| func rollKey(a *autoroll.AutoRollIssue) []byte { |
| return issueToRollKey(a.Issue) |
| } |
| |
| // timeToKey returns a BoltDB key for the given time.Time. |
| func timeToKey(t time.Time) []byte { |
| return []byte(t.Format(util.RFC3339NanoZeroPad)) |
| } |
| |
| // timeKey returns a BoltDB key for the given AutoRollIssue based on its |
| // last-modified time. |
| func timeKey(a *autoroll.AutoRollIssue) []byte { |
| return timeToKey(a.Created) |
| } |
| |
| // insertRoll inserts the given AutoRollIssue into the database within the |
| // given transaction. |
| func insertRoll(tx *bolt.Tx, a *autoroll.AutoRollIssue) error { |
| rolls := tx.Bucket(BUCKET_ROLLS) |
| rollsByDate := tx.Bucket(BUCKET_ROLLS_BY_DATE) |
| |
| serialized, err := json.Marshal(a) |
| if err != nil { |
| return err |
| } |
| |
| if err := rolls.Put(rollKey(a), serialized); err != nil { |
| return err |
| } |
| return rollsByDate.Put(timeKey(a), rollKey(a)) |
| } |
| |
| // deleteRoll deletes the given AutoRollIssue from the database within the |
| // given transaction. |
| func deleteRoll(tx *bolt.Tx, a *autoroll.AutoRollIssue) error { |
| rolls := tx.Bucket(BUCKET_ROLLS) |
| rollsByDate := tx.Bucket(BUCKET_ROLLS_BY_DATE) |
| |
| // Don't trust the created time of the passed-in roll; use the one we already have in the DB. |
| serialized := rolls.Get(rollKey(a)) |
| if serialized == nil { |
| return fmt.Errorf("The given issue (%d) does not exist in %s", a.Issue, string(BUCKET_ROLLS)) |
| } |
| var oldIssue autoroll.AutoRollIssue |
| if err := json.Unmarshal(serialized, &oldIssue); err != nil { |
| return err |
| } |
| |
| oldByDate := rollsByDate.Get(timeKey(&oldIssue)) |
| if oldByDate == nil { |
| return fmt.Errorf("The given issue (%d) does not exist in %s", a.Issue, string(BUCKET_ROLLS_BY_DATE)) |
| } |
| |
| if err := rollsByDate.Delete(timeKey(&oldIssue)); err != nil { |
| return err |
| } |
| if err := rolls.Delete(rollKey(a)); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // InsertRoll inserts the given AutoRollIssue into the database. |
| func (d *db) InsertRoll(a *autoroll.AutoRollIssue) error { |
| return d.db.Update(func(tx *bolt.Tx) error { |
| return insertRoll(tx, a) |
| }) |
| } |
| |
| // DeleteRoll deletes the given AutoRollIssue from the database. |
| func (d *db) DeleteRoll(a *autoroll.AutoRollIssue) error { |
| return d.db.Update(func(tx *bolt.Tx) error { |
| return deleteRoll(tx, a) |
| }) |
| } |
| |
| // UpdateRoll updates the given AutoRollIssue in the database. |
| func (d *db) UpdateRoll(a *autoroll.AutoRollIssue) error { |
| return d.db.Update(func(tx *bolt.Tx) error { |
| if err := deleteRoll(tx, a); err != nil { |
| return err |
| } |
| return insertRoll(tx, a) |
| }) |
| } |
| |
| // GetRoll retrieves the given issue from the database. |
| func (d *db) GetRoll(issue int64) (*autoroll.AutoRollIssue, error) { |
| var a *autoroll.AutoRollIssue |
| if err := d.db.View(func(tx *bolt.Tx) error { |
| b := tx.Bucket(BUCKET_ROLLS) |
| |
| serialized := b.Get(issueToRollKey(issue)) |
| if serialized == nil { |
| return nil |
| } |
| a = &autoroll.AutoRollIssue{} |
| if err := json.Unmarshal(serialized, a); err != nil { |
| return err |
| } |
| a.Created = a.Created.UTC() |
| a.Modified = a.Modified.UTC() |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| return a, nil |
| } |
| |
| // GetRecentRolls retrieves the most recent N issues from the database. |
| func (d *db) GetRecentRolls(N int) ([]*autoroll.AutoRollIssue, error) { |
| var rv []*autoroll.AutoRollIssue |
| if err := d.db.View(func(tx *bolt.Tx) error { |
| // Retrieve the issue keys from the by-date bucket. |
| byDate := tx.Bucket(BUCKET_ROLLS_BY_DATE) |
| c := byDate.Cursor() |
| keys := make([][]byte, 0, N) |
| for k, v := c.Last(); k != nil && len(keys) < N; k, v = c.Prev() { |
| keys = append(keys, v) |
| } |
| |
| // Retrieve the issues themselves. |
| b := tx.Bucket(BUCKET_ROLLS) |
| rv = make([]*autoroll.AutoRollIssue, 0, len(keys)) |
| for _, k := range keys { |
| serialized := b.Get(k) |
| if serialized == nil { |
| return fmt.Errorf("DB consistency error: bucket %s contains data not present in bucket %s!", BUCKET_ROLLS_BY_DATE, BUCKET_ROLLS) |
| } |
| var a autoroll.AutoRollIssue |
| if err := json.Unmarshal(serialized, &a); err != nil { |
| return err |
| } |
| a.Created = a.Created.UTC() |
| a.Modified = a.Modified.UTC() |
| rv = append(rv, &a) |
| } |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| return rv, nil |
| } |