Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions challenge-7/submissions/nzamulov/solution-template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Package challenge7 contains the solution for Challenge 7: Bank Account with Error Handling.
package challenge7

import "sync"

// BankAccount represents a bank account with balance management and minimum balance requirements.
type BankAccount struct {
ID string
Owner string
Balance float64
MinBalance float64
mu sync.Mutex // For thread safety
}

// Constants for account operations
const (
MaxTransactionAmount = 10000.0 // Example limit for deposits/withdrawals
)

// AccountError is a general error type for bank account operations.
type AccountError struct {
err string
}

func NewAccountError(err string) AccountError {
return AccountError{
err: err,
}
}

func (e AccountError) Error() string {
return "Account error: " + e.err
}

// InsufficientFundsError occurs when a withdrawal or transfer would bring the balance below minimum.
type InsufficientFundsError struct {}

func (e InsufficientFundsError) Error() string {
return "Insufficient funds error"
}

// NegativeAmountError occurs when an amount for deposit, withdrawal, or transfer is negative.
type NegativeAmountError struct {}

func (e NegativeAmountError) Error() string {
return "Negative amount error"
}

// ExceedsLimitError occurs when a deposit or withdrawal amount exceeds the defined limit.
type ExceedsLimitError struct {}

func (e ExceedsLimitError) Error() string {
return "Exceeds limit error"
}

// NewBankAccount creates a new bank account with the given parameters.
// It returns an error if any of the parameters are invalid.
func NewBankAccount(id, owner string, initialBalance, minBalance float64) (*BankAccount, error) {
if len(id) == 0 {
return nil, NewAccountError("empty id")
}
if len(owner) == 0 {
return nil, NewAccountError("empty owner")
}
if initialBalance < 0 {
return nil, NegativeAmountError{}
}
if minBalance < 0 {
return nil, NegativeAmountError{}
}
if initialBalance < minBalance {
return nil, InsufficientFundsError{}
}
return &BankAccount{
ID: id,
Owner: owner,
Balance: initialBalance,
MinBalance: minBalance,
}, nil
}

// Deposit adds the specified amount to the account balance.
// It returns an error if the amount is invalid or exceeds the transaction limit.
func (a *BankAccount) Deposit(amount float64) error {
if amount < 0 {
return NegativeAmountError{}
}
if amount > MaxTransactionAmount {
return ExceedsLimitError{}
}

a.mu.Lock()
defer a.mu.Unlock()

a.Balance += amount
return nil
}

// Withdraw removes the specified amount from the account balance.
// It returns an error if the amount is invalid, exceeds the transaction limit,
// or would bring the balance below the minimum required balance.
func (a *BankAccount) Withdraw(amount float64) error {
if amount < 0 {
return NegativeAmountError{}
}
if amount > MaxTransactionAmount {
return ExceedsLimitError{}
}

a.mu.Lock()
defer a.mu.Unlock()

if a.Balance - amount < a.MinBalance {
return InsufficientFundsError{}
}

a.Balance -= amount
return nil
}

// Transfer moves the specified amount from this account to the target account.
// It returns an error if the amount is invalid, exceeds the transaction limit,
// or would bring the balance below the minimum required balance.
func (a *BankAccount) Transfer(amount float64, target *BankAccount) error {
if target == nil {
return NewAccountError("target account is nil")
}
if a == target {
return NewAccountError("cannot transfer to same account")
}
if amount < 0 {
return NegativeAmountError{}
}
if amount > MaxTransactionAmount {
return ExceedsLimitError{}
}

first, second := a, target
if a.ID > target.ID {
first, second = target, a
}

first.mu.Lock()
defer first.mu.Unlock()
second.mu.Lock()
defer second.mu.Unlock()
Comment on lines +138 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Deadlock risk when two accounts share the same ID

Lock ordering swaps only when a.ID > target.ID. If two distinct accounts happen to share the same ID, goroutine A transferring from account1→account2 locks in the sequence (account1, account2) while goroutine B transferring from account2→account1 also locks (account2, account1). Both goroutines then block forever waiting on the other mutex. Because ID uniqueness is not enforced anywhere, this deadlock is reachable at runtime. To make the ordering unambiguous, add a deterministic tie-breaker (e.g., pointer address) when IDs compare equal so every transfer uses the same lock order.

-import "sync"
+import (
+	"sync"
+	"unsafe"
+)
@@
-	first, second := a, target
-	if a.ID > target.ID {
-		first, second = target, a
-	}
+	first, second := a, target
+	if a.ID > target.ID ||
+		(a.ID == target.ID && uintptr(unsafe.Pointer(a)) > uintptr(unsafe.Pointer(target))) {
+		first, second = target, a
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In challenge-7/submissions/nzamulov/solution-template.go around lines 138 to
146, the current lock-ordering swaps only when a.ID > target.ID which leaves a
deadlock when two distinct accounts share the same ID; change the ordering logic
to include a deterministic tie-breaker when IDs are equal (for example compare
the account object pointer addresses or another unique stable value) so that for
any pair of accounts you always choose the same first/second before locking;
implement the comparison so if a.ID == target.ID you fall back to comparing
uintptr(unsafe.Pointer(a)) vs uintptr(unsafe.Pointer(target)) (or another stable
unique key) and then lock first then second in that determined order.


if a.Balance - amount < a.MinBalance {
return InsufficientFundsError{}
}

a.Balance -= amount
target.Balance += amount
return nil
}
Loading