Add ledger task
This commit is contained in:
parent
72abaf8acd
commit
d65fd9bcf1
4 changed files with 184 additions and 0 deletions
19
ledger/README.md
Normal file
19
ledger/README.md
Normal 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
9
ledger/ledger.go
Normal 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
135
ledger/ledger_test.go
Normal 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
21
ledger/model.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue