diff --git a/ledger/README.md b/ledger/README.md new file mode 100644 index 0000000..eefe504 --- /dev/null +++ b/ledger/README.md @@ -0,0 +1,19 @@ +# ledger + +Реализуйте объект для хранения банковских счетов. Требуемый интерфейс находится в `model.go`. + +- Функция `New` должна создавать таблицу в базе данных. +- Метод `CreateAccount` должен создавать новый счёт с заданным `id`. +- Метод `GetBalance` должен возвращать текущий баланс. +- Метод `Deposit` должен зачислять деньги на счёт. +- Метод `Withdraw` должен снимать деньги со счёта. + Если на счету не достаточно денег, метод должен возвращать ошибку `ledger.ErrNoMoney`. +- Метод `Transfer` должен переводить деньги со счёта `from` на счёт `to`. + Если на счету `from` не достаточно денег, метод должен возвращать ошибку `ledger.ErrNoMoney`. + +Все операции должны быть атомарными. Для реализации некоторых методов +вам потребуется использовать транзакции и row-level локи. Ваша реализация не должна создавать дедлоки на уровне базы данных. + +Мы рекомендуем использовать функциональность `SELECT FOR UPDATE`. + +Комментарии по запуску postgres смотрите в задаче `dao`. \ No newline at end of file diff --git a/ledger/ledger.go b/ledger/ledger.go new file mode 100644 index 0000000..a82b6d1 --- /dev/null +++ b/ledger/ledger.go @@ -0,0 +1,9 @@ +//go:build !solution + +package ledger + +import "context" + +func New(ctx context.Context, dsn string) (Ledger, error) { + panic("not implemented") +} diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go new file mode 100644 index 0000000..feed4db --- /dev/null +++ b/ledger/ledger_test.go @@ -0,0 +1,135 @@ +package ledger_test + +import ( + "context" + "errors" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/slon/shad-go/ledger" + "gitlab.com/slon/shad-go/pgfixture" +) + +func TestLedger(t *testing.T) { + dsn := pgfixture.Start(t) + + ctx := context.Background() + + l0, err := ledger.New(ctx, dsn) + require.NoError(t, err) + + t.Run("SimpleCommands", func(t *testing.T) { + checkBalance := func(account ledger.ID, amount ledger.Money) { + b, err := l0.GetBalance(ctx, account) + require.NoError(t, err) + require.Equal(t, b, amount) + } + + require.NoError(t, l0.CreateAccount(ctx, "a0")) + checkBalance("a0", 0) + + require.Error(t, l0.CreateAccount(ctx, "a0")) + + require.NoError(t, l0.Deposit(ctx, "a0", ledger.Money(100))) + checkBalance("a0", 100) + + require.NoError(t, l0.Withdraw(ctx, "a0", ledger.Money(50))) + checkBalance("a0", 50) + + require.ErrorIs(t, l0.Withdraw(ctx, "a0", ledger.Money(100)), ledger.ErrNoMoney) + + require.NoError(t, l0.CreateAccount(ctx, "a1")) + + require.NoError(t, l0.Transfer(ctx, "a0", "a1", ledger.Money(40))) + checkBalance("a0", 10) + checkBalance("a1", 40) + + require.ErrorIs(t, l0.Transfer(ctx, "a0", "a1", ledger.Money(50)), ledger.ErrNoMoney) + }) + + t.Run("Transactions", func(t *testing.T) { + const nAccounts = 10 + const initialBalance = 5 + + var accounts []ledger.ID + for i := 0; i < nAccounts; i++ { + id := ledger.ID(fmt.Sprint(i)) + accounts = append(accounts, id) + + require.NoError(t, l0.CreateAccount(ctx, id)) + require.NoError(t, l0.Deposit(ctx, id, initialBalance)) + } + + var wg sync.WaitGroup + done := make(chan struct{}) + + spawn := func(action func() error) { + wg.Add(1) + + go func() { + defer wg.Done() + + for { + select { + case <-done: + return + + default: + if err := action(); err != nil { + if !errors.Is(err, ledger.ErrNoMoney) { + t.Errorf("operation failed: %v", err) + return + } + } + } + } + }() + } + + for i := 0; i < nAccounts; i++ { + i := i + + account := accounts[i] + next := accounts[(i+1)%len(accounts)] + prev := accounts[(i+len(accounts)-1)%len(accounts)] + + spawn(func() error { + balance, err := l0.GetBalance(ctx, account) + if err != nil { + return err + } + + if balance < 0 { + return fmt.Errorf("%q balance is negative", account) + } + + return nil + }) + + spawn(func() error { + return l0.Transfer(ctx, account, next, 1) + }) + + spawn(func() error { + return l0.Transfer(ctx, account, prev, 1) + }) + } + + time.Sleep(time.Second * 10) + close(done) + wg.Wait() + + var total ledger.Money + for i := 0; i < nAccounts; i++ { + amount, err := l0.GetBalance(ctx, accounts[i]) + require.NoError(t, err) + + total += amount + } + + require.Equal(t, total, ledger.Money(initialBalance*nAccounts)) + }) +} diff --git a/ledger/model.go b/ledger/model.go new file mode 100644 index 0000000..3bafcc0 --- /dev/null +++ b/ledger/model.go @@ -0,0 +1,21 @@ +package ledger + +import ( + "context" + "errors" +) + +type ( + ID string + Money int64 +) + +var ErrNoMoney = errors.New("no money") + +type Ledger interface { + CreateAccount(ctx context.Context, id ID) error + GetBalance(ctx context.Context, id ID) (Money, error) + Deposit(ctx context.Context, id ID, amount Money) error + Withdraw(ctx context.Context, id ID, amount Money) error + Transfer(ctx context.Context, from, to ID, amount Money) error +}