storage: use date-based upload ID
Instead of simply sequentially-increasing upload IDs, we now generate
one with a date prefix.
Change-Id: Id54ab88e6d76932cfc121183ea2da5d145599d16
Reviewed-on: https://go-review.googlesource.com/35256
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/app/upload_test.go b/storage/app/upload_test.go
index 5560c0e..0bc92ee 100644
--- a/storage/app/upload_test.go
+++ b/storage/app/upload_test.go
@@ -14,6 +14,7 @@
"net/http/httptest"
"reflect"
"testing"
+ "time"
"golang.org/x/perf/storage/db"
"golang.org/x/perf/storage/db/dbtest"
@@ -96,6 +97,8 @@
app := createTestApp(t)
defer app.Close()
+ wantID := time.Now().UTC().Format("20060102.") + "1"
+
status := app.uploadFiles(t, func(mpw *multipart.Writer) {
w, err := mpw.CreateFormFile("file", "1.txt")
if err != nil {
@@ -104,14 +107,14 @@
fmt.Fprintf(w, "key: value\nBenchmarkOne 5 ns/op\nkey:value2\nBenchmarkTwo 10 ns/op\n")
})
- if status.UploadID != "1" {
- t.Errorf("uploadid = %q, want %q", status.UploadID, "1")
+ if status.UploadID != wantID {
+ t.Errorf("uploadid = %q, want %q", status.UploadID, wantID)
}
- if have, want := status.FileIDs, []string{"1/0"}; !reflect.DeepEqual(have, want) {
+ if have, want := status.FileIDs, []string{wantID + "/0"}; !reflect.DeepEqual(have, want) {
t.Errorf("fileids = %v, want %v", have, want)
}
- if status.ViewURL != "view:1" {
- t.Errorf("viewurl = %q, want %q", status.ViewURL, "view:1")
+ if want := "view:" + wantID; status.ViewURL != want {
+ t.Errorf("viewurl = %q, want %q", status.ViewURL, want)
}
if len(app.fs.Files()) != 1 {
diff --git a/storage/db/db.go b/storage/db/db.go
index 0039ab8..bbbe812 100644
--- a/storage/db/db.go
+++ b/storage/db/db.go
@@ -12,8 +12,10 @@
"errors"
"fmt"
"io"
+ "strconv"
"strings"
"text/template"
+ "time"
"unicode"
"golang.org/x/net/context"
@@ -27,6 +29,7 @@
type DB struct {
sql *sql.DB // underlying database connection
// prepared statements
+ lastUpload *sql.Stmt
insertUpload *sql.Stmt
insertRecord *sql.Stmt
}
@@ -69,17 +72,25 @@
// entry whose key is the driver name.
var createTmpl = template.Must(template.New("create").Parse(`
CREATE TABLE IF NOT EXISTS Uploads (
- UploadID {{if .sqlite3}}INTEGER PRIMARY KEY AUTOINCREMENT{{else}}SERIAL PRIMARY KEY AUTO_INCREMENT{{end}}
+ UploadID VARCHAR(20) PRIMARY KEY,
+ Day VARCHAR(8),
+ Seq BIGINT UNSIGNED
+{{if not .sqlite3}}
+ , Index (Day, Seq)
+{{end}}
);
+{{if .sqlite3}}
+CREATE INDEX IF NOT EXISTS UploadDaySeq ON Uploads(Day, Seq);
+{{end}}
CREATE TABLE IF NOT EXISTS Records (
- UploadID BIGINT UNSIGNED,
+ UploadID VARCHAR(20),
RecordID BIGINT UNSIGNED,
Content BLOB,
PRIMARY KEY (UploadID, RecordID),
FOREIGN KEY (UploadID) REFERENCES Uploads(UploadID) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS RecordLabels (
- UploadID BIGINT UNSIGNED,
+ UploadID VARCHAR(20),
RecordID BIGINT UNSIGNED,
Name VARCHAR(255),
Value VARCHAR(8192),
@@ -115,11 +126,15 @@
// prepareStatements calls db.sql.Prepare on reusable SQL statements.
func (db *DB) prepareStatements(driverName string) error {
var err error
- q := "INSERT INTO Uploads() VALUES ()"
- if driverName == "sqlite3" {
- q = "INSERT INTO Uploads DEFAULT VALUES"
+ query := "SELECT UploadID FROM Uploads ORDER BY Day DESC, Seq DESC LIMIT 1"
+ if driverName != "sqlite3" {
+ query += " FOR UPDATE"
}
- db.insertUpload, err = db.sql.Prepare(q)
+ db.lastUpload, err = db.sql.Prepare(query)
+ if err != nil {
+ return err
+ }
+ db.insertUpload, err = db.sql.Prepare("INSERT INTO Uploads(UploadID, Day, Seq) VALUES (?, ?, ?)")
if err != nil {
return err
}
@@ -136,11 +151,6 @@
// associated with every record in this upload.
ID string
- // id is the numeric value used as the primary key. ID is a
- // string for the public API; the underlying table actually
- // uses an integer key. To avoid repeated calls to
- // strconv.Atoi, the int64 is cached here.
- id int64
// recordid is the index of the next record to insert.
recordid int64
// db is the underlying database that this upload is going to.
@@ -149,28 +159,61 @@
tx *sql.Tx
}
+// now is a hook for testing
+var now = time.Now
+
// NewUpload returns an upload for storing new files.
// All records written to the Upload will have the same upload ID.
func (db *DB) NewUpload(ctx context.Context) (*Upload, error) {
- // TODO(quentin): Use the same transaction as the rest of the upload?
- res, err := db.insertUpload.Exec()
- if err != nil {
- return nil, err
- }
- // TODO(quentin): Use a date-based upload ID (YYYYMMDDnnn)
- i, err := res.LastInsertId()
- if err != nil {
- return nil, err
- }
+ day := now().UTC().Format("20060102")
+
+ num := 0
+
tx, err := db.sql.Begin()
if err != nil {
return nil, err
}
+ defer func() {
+ if tx != nil {
+ tx.Rollback()
+ }
+ }()
+ var lastID string
+ err = tx.Stmt(db.lastUpload).QueryRow().Scan(&lastID)
+ switch err {
+ case sql.ErrNoRows:
+ case nil:
+ if strings.HasPrefix(lastID, day) {
+ num, err = strconv.Atoi(lastID[len(day)+1:])
+ if err != nil {
+ return nil, err
+ }
+ }
+ default:
+ return nil, err
+ }
+
+ num++
+
+ id := fmt.Sprintf("%s.%d", day, num)
+
+ _, err = tx.Stmt(db.insertUpload).Exec(id, day, num)
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, err
+ }
+ tx = nil
+
+ utx, err := db.sql.Begin()
+ if err != nil {
+ return nil, err
+ }
return &Upload{
- ID: fmt.Sprint(i),
- id: i,
+ ID: id,
db: db,
- tx: tx,
+ tx: utx,
}, nil
}
@@ -182,15 +225,15 @@
if err := benchfmt.NewPrinter(&buf).Print(r); err != nil {
return err
}
- if _, err := u.tx.Stmt(u.db.insertRecord).Exec(u.id, u.recordid, buf.Bytes()); err != nil {
+ if _, err := u.tx.Stmt(u.db.insertRecord).Exec(u.ID, u.recordid, buf.Bytes()); err != nil {
return err
}
var args []interface{}
for _, k := range r.Labels.Keys() {
- args = append(args, u.id, u.recordid, k, r.Labels[k])
+ args = append(args, u.ID, u.recordid, k, r.Labels[k])
}
for _, k := range r.NameLabels.Keys() {
- args = append(args, u.id, u.recordid, k, r.NameLabels[k])
+ args = append(args, u.ID, u.recordid, k, r.NameLabels[k])
}
if len(args) > 0 {
query := "INSERT INTO RecordLabels VALUES " + strings.Repeat("(?, ?, ?, ?), ", len(args)/4)
diff --git a/storage/db/db_test.go b/storage/db/db_test.go
index a3e7bd9..13dd630 100644
--- a/storage/db/db_test.go
+++ b/storage/db/db_test.go
@@ -10,6 +10,7 @@
"reflect"
"strings"
"testing"
+ "time"
"golang.org/x/perf/storage/benchfmt"
. "golang.org/x/perf/storage/db"
@@ -36,8 +37,52 @@
}
}
+// TestUploadIDs verifies that NewUpload generates the correct sequence of upload IDs.
+func TestUploadIDs(t *testing.T) {
+ ctx := context.Background()
+
+ db, cleanup := dbtest.NewDB(t)
+ defer cleanup()
+
+ defer SetNow(time.Time{})
+
+ tests := []struct {
+ sec int64
+ id string
+ }{
+ {0, "19700101.1"},
+ {0, "19700101.2"},
+ {86400, "19700102.1"},
+ {86400, "19700102.2"},
+ {86400, "19700102.3"},
+ {86400, "19700102.4"},
+ {86400, "19700102.5"},
+ {86400, "19700102.6"},
+ {86400, "19700102.7"},
+ {86400, "19700102.8"},
+ {86400, "19700102.9"},
+ {86400, "19700102.10"},
+ {86400, "19700102.11"},
+ }
+ for _, test := range tests {
+ SetNow(time.Unix(test.sec, 0))
+ u, err := db.NewUpload(ctx)
+ if err != nil {
+ t.Fatalf("NewUpload: %v", err)
+ }
+ if err := u.Commit(); err != nil {
+ t.Fatalf("Commit: %v", err)
+ }
+ if u.ID != test.id {
+ t.Fatalf("u.ID = %q, want %q", u.ID, test.id)
+ }
+ }
+}
+
// TestNewUpload verifies that NewUpload and InsertRecord wrote the correct rows to the database.
func TestNewUpload(t *testing.T) {
+ SetNow(time.Unix(0, 0))
+ defer SetNow(time.Time{})
db, cleanup := dbtest.NewDB(t)
defer cleanup()
@@ -78,14 +123,15 @@
i := 0
for rows.Next() {
- var uploadid, recordid int64
+ var uploadid string
+ var recordid int64
var name, value string
if err := rows.Scan(&uploadid, &recordid, &name, &value); err != nil {
- t.Fatalf("rows.Scan: %v")
+ t.Fatalf("rows.Scan: %v", err)
}
- if uploadid != 1 {
- t.Errorf("uploadid = %d, want 1", uploadid)
+ if uploadid != "19700101.1" {
+ t.Errorf("uploadid = %q, want %q", uploadid, "19700101.1")
}
if recordid != 0 {
t.Errorf("recordid = %d, want 0", recordid)
diff --git a/storage/db/export_test.go b/storage/db/export_test.go
index 49e54b8..5f465f0 100644
--- a/storage/db/export_test.go
+++ b/storage/db/export_test.go
@@ -4,10 +4,21 @@
package db
-import "database/sql"
+import (
+ "database/sql"
+ "time"
+)
var SplitQueryWords = splitQueryWords
func DBSQL(db *DB) *sql.DB {
return db.sql
}
+
+func SetNow(t time.Time) {
+ if t.IsZero() {
+ now = time.Now
+ return
+ }
+ now = func() time.Time { return t }
+}