From 957db15ef4cef2f3a6b2933260a7806d69ff68e0 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Mon, 4 Nov 2013 10:28:38 -0800 Subject: [PATCH] Add lock around write operations in graph The graph uses a persistent database connection so a lock is required because our current sqlite3 driver does not implement retry logic when the ErrBusy is received. --- gograph/gograph.go | 17 +++++++++++++++-- gograph/gograph_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/gograph/gograph.go b/gograph/gograph.go index 94ce90afb4..32b3a491bb 100644 --- a/gograph/gograph.go +++ b/gograph/gograph.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "path" + "sync" ) const ( @@ -47,6 +48,7 @@ type WalkFunc func(fullPath string, entity *Entity) error // Graph database for storing entities and their relationships type Database struct { conn *sql.DB + mux sync.Mutex } // Create a new graph database initialized with a root entity @@ -54,7 +56,7 @@ func NewDatabase(conn *sql.DB, init bool) (*Database, error) { if conn == nil { return nil, fmt.Errorf("Database connection cannot be nil") } - db := &Database{conn} + db := &Database{conn: conn} if init { if _, err := conn.Exec(createEntityTable); err != nil { @@ -99,7 +101,9 @@ func (db *Database) Close() error { // Set the entity id for a given path func (db *Database) Set(fullPath, id string) (*Entity, error) { - // FIXME: is rollback implicit when closing the connection? + db.mux.Lock() + defer db.mux.Unlock() + rollback := func() { db.conn.Exec("ROLLBACK") } @@ -256,6 +260,9 @@ func (db *Database) RefPaths(id string) Edges { // Delete the reference to an entity at a given path func (db *Database) Delete(name string) error { + db.mux.Lock() + defer db.mux.Unlock() + if name == "/" { return fmt.Errorf("Cannot delete root entity") } @@ -276,6 +283,9 @@ func (db *Database) Delete(name string) error { // Walk the graph to make sure all references to the entity // are removed and return the number of references removed func (db *Database) Purge(id string) (int, error) { + db.mux.Lock() + defer db.mux.Unlock() + rollback := func() { db.conn.Exec("ROLLBACK") } @@ -310,6 +320,9 @@ func (db *Database) Purge(id string) (int, error) { // Rename an edge for a given path func (db *Database) Rename(currentName, newName string) error { + db.mux.Lock() + defer db.mux.Unlock() + parentPath, name := splitPath(currentName) newParentPath, newEdgeName := splitPath(newName) diff --git a/gograph/gograph_test.go b/gograph/gograph_test.go index dba8ed1080..1a40fffce3 100644 --- a/gograph/gograph_test.go +++ b/gograph/gograph_test.go @@ -3,6 +3,7 @@ package gograph import ( _ "code.google.com/p/gosqlite/sqlite3" "database/sql" + "fmt" "os" "path" "strconv" @@ -501,3 +502,39 @@ func TestGetNameWithTrailingSlash(t *testing.T) { t.Fatalf("Entity should not be nil") } } + +func TestConcurrentWrites(t *testing.T) { + db, dbpath := newTestDb(t) + defer destroyTestDb(dbpath) + + errs := make(chan error, 2) + + save := func(name string, id string) { + if _, err := db.Set(fmt.Sprintf("/%s", name), id); err != nil { + errs <- err + } + errs <- nil + } + purge := func(id string) { + if _, err := db.Purge(id); err != nil { + errs <- err + } + errs <- nil + } + + save("/1", "1") + + go purge("1") + go save("/2", "2") + + any := false + for i := 0; i < 2; i++ { + if err := <-errs; err != nil { + any = true + t.Log(err) + } + } + if any { + t.Fatal() + } +}