Add ledger task

This commit is contained in:
Fedor Korotkiy 2022-03-31 14:19:47 +03:00
parent 72abaf8acd
commit d65fd9bcf1
4 changed files with 184 additions and 0 deletions

19
ledger/README.md Normal file
View file

@ -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`.

9
ledger/ledger.go Normal file
View file

@ -0,0 +1,9 @@
//go:build !solution
package ledger
import "context"
func New(ctx context.Context, dsn string) (Ledger, error) {
panic("not implemented")
}

135
ledger/ledger_test.go Normal file
View file

@ -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))
})
}

21
ledger/model.go Normal file
View file

@ -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
}