Make item updates transactional

This commit is contained in:
Tim Van Baak 2025-01-31 09:55:07 -08:00
parent b769d71f6e
commit 37142229ea
4 changed files with 79 additions and 12 deletions

View File

@ -143,11 +143,15 @@ func actionExecute(
return
}
if err = core.UpdateItems(db, []core.Item{newItem}); err != nil {
log.Fatalf("error: failed to update item: %v", err)
}
if err = core.SetState(db, source, newState); err != nil {
log.Fatalf("error: failed to set state for %s: %v", source, err)
if err = db.Transact(func(tx core.DB) error {
if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil {
return fmt.Errorf("failed to update item: %v", _err)
}
if _err := core.SetState(tx, source, newState); err != nil {
return fmt.Errorf("failed to set state for %s: %v", source, _err)
}
return nil
}); err != nil {
log.Fatalf("error: %v", err)
}
}

View File

@ -254,6 +254,18 @@ func SetState(db DB, source string, state []byte) error {
//
// Returns the number of new and deleted items on success.
func UpdateWithFetchedItems(db DB, source string, state []byte, items []Item) (int, int, error) {
var new int
var del int
var err error
err = db.Transact(func(tx DB) error {
new, del, err = updateWithFetchedItemsTx(tx, source, state, items)
return err
})
return new, del, err
}
// Implementation logic for [UpdateWithFetchedItems], which executes this inside a transaction.
func updateWithFetchedItemsTx(db DB, source string, state []byte, items []Item) (int, int, error) {
// Get the existing items
existingItems, err := GetAllItemsForSource(db, source)
if err != nil {

View File

@ -1,8 +1,10 @@
package core
import (
"errors"
"fmt"
"slices"
"strings"
"testing"
"time"
@ -163,6 +165,51 @@ func TestUpdateSourceAddAndDelete(t *testing.T) {
}
}
func TestUpdateSourceTransaction(t *testing.T) {
db := EphemeralDb(t)
if err := AddSource(db, "s"); err != nil {
t.Fatal(err)
}
a := Item{Source: "s", Id: "a"}
b := Item{Source: "s", Id: "b"}
// Add and deactivate a so it will be deleted on next fetch without it
if add, _, err := UpdateWithFetchedItems(db, "s", nil, []Item{a}); add != 1 || err != nil {
t.Fatalf("expected 1 add, got %d and err %v", add, err)
}
if _, err := DeactivateItem(db, "s", "a"); err != nil {
t.Fatal(err)
}
// Add b and cause a to be deleted, but the delete throws an error
fdb := &FailureDb{
db: db,
execError: func(q string, a ...any) error {
if strings.Contains(q, "delete from") {
return errors.New("no deletes!")
}
return nil
},
}
add, del, err := UpdateWithFetchedItems(fdb, "s", nil, []Item{b})
if add != 0 || del != 0 || err == nil {
t.Fatalf("expected failure, got %d %d %v", add, del, err)
}
// Failure should not add b
items, err := GetAllItemsForSource(db, "s")
if err != nil {
t.Fatal(err)
}
if len(items) != 1 {
t.Fatalf("should only have one item, got %d", len((items)))
}
if items[0].Id != "a" {
t.Fatalf("expected only item to still be a, got %s", items[0].Id)
}
}
func TestOnCreateAction(t *testing.T) {
db := EphemeralDb(t)
if err := AddSource(db, "test"); err != nil {

View File

@ -1,6 +1,7 @@
package web
import (
"fmt"
"log"
"net/http"
"strings"
@ -77,13 +78,16 @@ func (env *Env) doAction(writer http.ResponseWriter, req *http.Request) {
return
}
if err = core.UpdateItems(env.db, []core.Item{newItem}); err != nil {
if err = env.db.Transact(func(tx core.DB) error {
if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil {
return fmt.Errorf("failed to update item: %v", _err)
}
if _err := core.SetState(tx, source, newState); err != nil {
return fmt.Errorf("failed to set state for %s: %v", source, _err)
}
return nil
}); err != nil {
http.Error(writer, err.Error(), 500)
return
}
if err = core.SetState(env.db, source, newState); err != nil {
log.Fatalf("error: failed to set state for %s: %v", source, err)
}
html.Item(writer, html.ItemData{Item: newItem})