🏡 index : quine-global/quintodrome.git

package persistence

import (
	"context"
	"errors"
	"time"

	"github.com/mattn/go-sqlite3"
	"github.com/navidrome/navidrome/db"
	"github.com/navidrome/navidrome/model"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("SQLStore", func() {
	var ds model.DataStore
	var ctx context.Context
	BeforeEach(func() {
		ds = New(db.Db())
		ctx = context.Background()
	})
	Describe("WithTx", func() {
		Context("When block returns nil", func() {
			It("commits changes to the DB", func() {
				err := ds.WithTx(func(tx model.DataStore) error {
					pl := tx.Player()
					err := pl.Put(ctx, &model.Player{ID: "666", UserId: "userid"})
					Expect(err).ToNot(HaveOccurred())

					pr := tx.Property()
					err = pr.Put(ctx, "777", "value")
					Expect(err).ToNot(HaveOccurred())
					return nil
				})
				Expect(err).ToNot(HaveOccurred())
				Expect(ds.Player().Get(ctx, "666")).To(Equal(&model.Player{ID: "666", UserId: "userid", Username: "userid"}))
				Expect(ds.Property().Get(ctx, "777")).To(Equal("value"))
			})
		})
		Context("When block returns an error", func() {
			It("rollbacks changes to the DB", func() {
				err := ds.WithTx(func(tx model.DataStore) error {
					pr := tx.Property()
					err := pr.Put(ctx, "999", "value")
					Expect(err).ToNot(HaveOccurred())

					// Will fail as it is missing the UserName
					pl := tx.Player()
					err = pl.Put(ctx, &model.Player{ID: "888"})
					Expect(err).To(HaveOccurred())
					return err
				})
				Expect(err).To(HaveOccurred())
				_, err = ds.Property().Get(ctx, "999")
				Expect(err).To(MatchError(model.ErrNotFound))
				_, err = ds.Player().Get(ctx, "888")
				Expect(err).To(MatchError(model.ErrNotFound))
			})
		})
	})

	Describe("WithTxRetry", func() {
		busy := sqlite3.Error{Code: sqlite3.ErrBusy}
		BeforeEach(func() {
			DeferCleanup(func(d time.Duration) { txRetryDelay = d }, txRetryDelay)
			txRetryDelay = 0
		})

		It("reruns a busy transaction from a clean rollback", func() {
			var attempts []bool
			err := ds.WithTxRetry(ctx, func(ctx context.Context, tx model.DataStore) error {
				attempts = append(attempts, hasBusyRetry(ctx))
				Expect(tx.Property().Put(ctx, "retry-key", "attempt")).To(Succeed())
				if len(attempts) < 3 {
					return busy
				}
				return nil
			})
			Expect(err).ToNot(HaveOccurred())
			Expect(attempts).To(Equal([]bool{true, true, true}))
			Expect(ds.Property().Get(ctx, "retry-key")).To(Equal("attempt"))
		})

		It("gives up after the last retry, which is not marked as retried", func() {
			var attempts []bool
			err := ds.WithTxRetry(ctx, func(ctx context.Context, _ model.DataStore) error {
				attempts = append(attempts, hasBusyRetry(ctx))
				return busy
			})
			Expect(db.IsBusy(err)).To(BeTrue())
			Expect(attempts).To(Equal([]bool{true, true, true, false}))
		})

		It("does not rerun on other errors", func() {
			calls := 0
			err := ds.WithTxRetry(ctx, func(context.Context, model.DataStore) error {
				calls++
				return sqlite3.Error{Code: sqlite3.ErrConstraint}
			})
			Expect(err).To(HaveOccurred())
			Expect(calls).To(Equal(1))
		})

		It("does not rerun when called inside a transaction", func() {
			calls := 0
			err := ds.WithTx(func(tx model.DataStore) error {
				return tx.WithTxRetry(ctx, func(context.Context, model.DataStore) error {
					calls++
					return busy
				})
			})
			Expect(db.IsBusy(err)).To(BeTrue())
			Expect(calls).To(Equal(1))
		})

		It("joins the enclosing transaction instead of opening another", func() {
			rollback := errors.New("rollback")
			err := ds.WithTx(func(tx model.DataStore) error {
				Expect(tx.Property().Put(ctx, "outer-key", "v")).To(Succeed())
				Expect(tx.WithTxRetry(ctx, func(ctx context.Context, inner model.DataStore) error {
					Expect(inner.Property().Get(ctx, "outer-key")).To(Equal("v"))
					return inner.Property().Put(ctx, "inner-key", "v")
				})).To(Succeed())
				return rollback
			})
			Expect(err).To(MatchError(rollback))
			_, err = ds.Property().Get(ctx, "inner-key")
			Expect(err).To(MatchError(model.ErrNotFound))
		})
	})
})