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