split proto into two; move more logic into db.go to make bank service impl cleaner; add graceful shutdown on signal
This commit is contained in:
parent
e1a9991958
commit
a020c24a83
|
|
@ -34,30 +34,7 @@ func (s *bankServer) OpenAccount(ctx context.Context, req *OpenAccountRequest) (
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid account type: %v", req.Type)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid account type: %v", req.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.allAccounts.mu.Lock()
|
return s.allAccounts.openAccount(cust, req.Type, req.InitialDepositCents), nil
|
||||||
defer s.allAccounts.mu.Unlock()
|
|
||||||
accountNums, ok := s.allAccounts.AccountNumbersByCustomer[cust]
|
|
||||||
if !ok {
|
|
||||||
// no accounts for this customer? it's a new customer
|
|
||||||
s.allAccounts.Customers = append(s.allAccounts.Customers, cust)
|
|
||||||
}
|
|
||||||
num := s.allAccounts.LastAccountNum + 1
|
|
||||||
s.allAccounts.LastAccountNum = num
|
|
||||||
s.allAccounts.AccountNumbers = append(s.allAccounts.AccountNumbers, num)
|
|
||||||
accountNums = append(accountNums, num)
|
|
||||||
s.allAccounts.AccountNumbersByCustomer[cust] = accountNums
|
|
||||||
var acct account
|
|
||||||
acct.AccountNumber = num
|
|
||||||
acct.BalanceCents = req.InitialDepositCents
|
|
||||||
acct.Transactions = append(acct.Transactions, &Transaction{
|
|
||||||
AccountNumber: num,
|
|
||||||
SeqNumber: 1,
|
|
||||||
Date: ptypes.TimestampNow(),
|
|
||||||
AmountCents: req.InitialDepositCents,
|
|
||||||
Desc: "initial deposit",
|
|
||||||
})
|
|
||||||
s.allAccounts.AccountsByNumber[num] = &acct
|
|
||||||
return &acct.Account, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *bankServer) CloseAccount(ctx context.Context, req *CloseAccountRequest) (*empty.Empty, error) {
|
func (s *bankServer) CloseAccount(ctx context.Context, req *CloseAccountRequest) (*empty.Empty, error) {
|
||||||
|
|
@ -66,33 +43,9 @@ func (s *bankServer) CloseAccount(ctx context.Context, req *CloseAccountRequest)
|
||||||
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
|
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
s.allAccounts.mu.Lock()
|
if err := s.allAccounts.closeAccount(cust, req.AccountNumber); err != nil {
|
||||||
defer s.allAccounts.mu.Unlock()
|
return nil, err
|
||||||
acctNums := s.allAccounts.AccountNumbersByCustomer[cust]
|
|
||||||
found := -1
|
|
||||||
for i, num := range acctNums {
|
|
||||||
if num == req.AccountNumber {
|
|
||||||
found = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if found == -1 {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", req.AccountNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, num := range s.allAccounts.AccountNumbers {
|
|
||||||
if num == req.AccountNumber {
|
|
||||||
s.allAccounts.AccountNumbers = append(s.allAccounts.AccountNumbers[:i], s.allAccounts.AccountNumbers[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acct := s.allAccounts.AccountsByNumber[req.AccountNumber]
|
|
||||||
if acct.BalanceCents != 0 {
|
|
||||||
return nil, status.Errorf(codes.FailedPrecondition, "account %d cannot be closed because it has a non-zero balance: %s", req.AccountNumber, dollars(acct.BalanceCents))
|
|
||||||
}
|
|
||||||
s.allAccounts.AccountNumbersByCustomer[cust] = append(acctNums[:found], acctNums[found+1:]...)
|
|
||||||
delete(s.allAccounts.AccountsByNumber, req.AccountNumber)
|
|
||||||
return &empty.Empty{}, nil
|
return &empty.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,13 +55,7 @@ func (s *bankServer) GetAccounts(ctx context.Context, _ *empty.Empty) (*GetAccou
|
||||||
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
|
return nil, status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
s.allAccounts.mu.RLock()
|
accounts := s.allAccounts.getAllAccounts(cust)
|
||||||
defer s.allAccounts.mu.RUnlock()
|
|
||||||
accountNums := s.allAccounts.AccountNumbersByCustomer[cust]
|
|
||||||
var accounts []*Account
|
|
||||||
for _, num := range accountNums {
|
|
||||||
accounts = append(accounts, &s.allAccounts.AccountsByNumber[num].Account)
|
|
||||||
}
|
|
||||||
return &GetAccountsResponse{Accounts: accounts}, nil
|
return &GetAccountsResponse{Accounts: accounts}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,17 +65,7 @@ func (s *bankServer) GetTransactions(req *GetTransactionsRequest, stream Bank_Ge
|
||||||
return status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
|
return status.Error(codes.Unauthenticated, codes.Unauthenticated.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
acct, err := func() (*account, error) {
|
acct, err := s.allAccounts.getAccount(cust, req.AccountNumber)
|
||||||
s.allAccounts.mu.Lock()
|
|
||||||
defer s.allAccounts.mu.Unlock()
|
|
||||||
acctNums := s.allAccounts.AccountNumbersByCustomer[cust]
|
|
||||||
for _, num := range acctNums {
|
|
||||||
if num == req.AccountNumber {
|
|
||||||
return s.allAccounts.AccountsByNumber[num], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", req.AccountNumber)
|
|
||||||
}()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -149,10 +86,7 @@ func (s *bankServer) GetTransactions(req *GetTransactionsRequest, stream Bank_Ge
|
||||||
end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.Local)
|
end = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.Local)
|
||||||
}
|
}
|
||||||
|
|
||||||
acct.mu.RLock()
|
txns := acct.getTransactions()
|
||||||
txns := acct.Transactions
|
|
||||||
acct.mu.RUnlock()
|
|
||||||
|
|
||||||
for _, txn := range txns {
|
for _, txn := range txns {
|
||||||
t, err := ptypes.Timestamp(txn.Date)
|
t, err := ptypes.Timestamp(txn.Date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -190,7 +124,11 @@ func (s *bankServer) Deposit(ctx context.Context, req *DepositRequest) (*Balance
|
||||||
if req.Desc != "" {
|
if req.Desc != "" {
|
||||||
desc = fmt.Sprintf("%s: %s", desc, req.Desc)
|
desc = fmt.Sprintf("%s: %s", desc, req.Desc)
|
||||||
}
|
}
|
||||||
newBalance, err := s.newTransaction(cust, req.AccountNumber, req.AmountCents, desc)
|
acct, err := s.allAccounts.getAccount(cust, req.AccountNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newBalance, err := acct.newTransaction(req.AmountCents, desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -200,26 +138,6 @@ func (s *bankServer) Deposit(ctx context.Context, req *DepositRequest) (*Balance
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *bankServer) getAccount(cust string, acctNumber uint64) (*account, error) {
|
|
||||||
s.allAccounts.mu.Lock()
|
|
||||||
defer s.allAccounts.mu.Unlock()
|
|
||||||
acctNums := s.allAccounts.AccountNumbersByCustomer[cust]
|
|
||||||
for _, num := range acctNums {
|
|
||||||
if num == acctNumber {
|
|
||||||
return s.allAccounts.AccountsByNumber[num], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", acctNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *bankServer) newTransaction(cust string, acctNumber uint64, amountCents int32, desc string) (int32, error) {
|
|
||||||
acct, err := s.getAccount(cust, acctNumber)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return acct.newTransaction(amountCents, desc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*BalanceResponse, error) {
|
func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*BalanceResponse, error) {
|
||||||
cust := getCustomer(ctx)
|
cust := getCustomer(ctx)
|
||||||
if cust == "" {
|
if cust == "" {
|
||||||
|
|
@ -230,7 +148,11 @@ func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*Balan
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "withdrawal amount cannot be non-negative: %s", dollars(req.AmountCents))
|
return nil, status.Errorf(codes.InvalidArgument, "withdrawal amount cannot be non-negative: %s", dollars(req.AmountCents))
|
||||||
}
|
}
|
||||||
|
|
||||||
newBalance, err := s.newTransaction(cust, req.AccountNumber, req.AmountCents, req.Desc)
|
acct, err := s.allAccounts.getAccount(cust, req.AccountNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newBalance, err := acct.newTransaction(req.AmountCents, req.Desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -240,10 +162,6 @@ func (s *bankServer) Withdraw(ctx context.Context, req *WithdrawRequest) (*Balan
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dollars(amountCents int32) string {
|
|
||||||
return fmt.Sprintf("$%02f", float64(amountCents)/100)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*TransferResponse, error) {
|
func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*TransferResponse, error) {
|
||||||
cust := getCustomer(ctx)
|
cust := getCustomer(ctx)
|
||||||
if cust == "" {
|
if cust == "" {
|
||||||
|
|
@ -265,7 +183,7 @@ func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*Trans
|
||||||
case *TransferRequest_SourceAccountNumber:
|
case *TransferRequest_SourceAccountNumber:
|
||||||
srcDesc = fmt.Sprintf("account %06d", src.SourceAccountNumber)
|
srcDesc = fmt.Sprintf("account %06d", src.SourceAccountNumber)
|
||||||
var err error
|
var err error
|
||||||
if srcAcct, err = s.getAccount(cust, src.SourceAccountNumber); err != nil {
|
if srcAcct, err = s.allAccounts.getAccount(cust, src.SourceAccountNumber); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +199,7 @@ func (s *bankServer) Transfer(ctx context.Context, req *TransferRequest) (*Trans
|
||||||
case *TransferRequest_DestAccountNumber:
|
case *TransferRequest_DestAccountNumber:
|
||||||
destDesc = fmt.Sprintf("account %06d", dest.DestAccountNumber)
|
destDesc = fmt.Sprintf("account %06d", dest.DestAccountNumber)
|
||||||
var err error
|
var err error
|
||||||
if destAcct, err = s.getAccount(cust, dest.DestAccountNumber); err != nil {
|
if destAcct, err = s.allAccounts.getAccount(cust, dest.DestAccountNumber); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -33,24 +33,6 @@ service Bank {
|
||||||
rpc Transfer(TransferRequest) returns (TransferResponse);
|
rpc Transfer(TransferRequest) returns (TransferResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support provides an interactive chat service, for customers to interact with
|
|
||||||
// the bank's support agents. A single stream, for either of the two methods, is
|
|
||||||
// a stateful connection to a single "chat session". Streams are initially disconnected
|
|
||||||
// (not part of any session). A stream must be disconnected from a session (via customer
|
|
||||||
// hang up or via agent leaving a session) before it can be connected to a new one.
|
|
||||||
service Support {
|
|
||||||
// ChatCustomer is used by a customer-facing app to send the customer's messages
|
|
||||||
// to a chat session. The customer is how initiates and terminates (via "hangup")
|
|
||||||
// a chat session. Only customers may invoke this method (e.g. requests must
|
|
||||||
// include customer auth credentials).
|
|
||||||
rpc ChatCustomer(stream ChatCustomerRequest) returns (stream ChatCustomerResponse);
|
|
||||||
// ChatAgent is used by an agent-facing app to allow an agent to reply to a
|
|
||||||
// customer's messages in a chat session. The agent may accept a chat session,
|
|
||||||
// which defaults to the session awaiting an agent for the longest period of time
|
|
||||||
// (FIFO queue).
|
|
||||||
rpc ChatAgent(stream ChatAgentRequest) returns (stream ChatAgentResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
message OpenAccountRequest {
|
message OpenAccountRequest {
|
||||||
int32 initial_deposit_cents = 1;
|
int32 initial_deposit_cents = 1;
|
||||||
Account.Type type = 2;
|
Account.Type type = 2;
|
||||||
|
|
@ -141,98 +123,3 @@ message TransferResponse {
|
||||||
uint64 dest_account_number = 3;
|
uint64 dest_account_number = 3;
|
||||||
int32 dest_balance_cents = 4;
|
int32 dest_balance_cents = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Void {
|
|
||||||
VOID = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatCustomerRequest {
|
|
||||||
oneof req {
|
|
||||||
// init is used when a chat stream is not part of a
|
|
||||||
// chat session. This is a stream's initial state, as well as
|
|
||||||
// the state after a "hang_up" request is sent. This creates
|
|
||||||
// a new state session or resumes an existing one.
|
|
||||||
InitiateChat init = 1;
|
|
||||||
// msg is used to send the customer's messages to support
|
|
||||||
// agents.
|
|
||||||
string msg = 2;
|
|
||||||
// hang_up is used to terminate a chat session. If a stream
|
|
||||||
// is broken, but the session was not terminated, the client
|
|
||||||
// may initiate a new stream and use init to resume that
|
|
||||||
// session. Sessions are not terminated unless done so
|
|
||||||
// explicitly via sending this kind of request on the stream.
|
|
||||||
Void hang_up = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message InitiateChat {
|
|
||||||
string resume_session_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AgentMessage {
|
|
||||||
string agent_name = 1;
|
|
||||||
string msg = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatCustomerResponse {
|
|
||||||
oneof resp {
|
|
||||||
// session is sent from the server when the stream is connected
|
|
||||||
// to a chat session. This happens after an init request is sent
|
|
||||||
// and the stream is connected to either a new or resumed session.
|
|
||||||
Session session = 1;
|
|
||||||
// msg is sent from the server to convey agents' messages to the
|
|
||||||
// customer.
|
|
||||||
AgentMessage msg = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatAgentRequest {
|
|
||||||
oneof req {
|
|
||||||
// accept is used when an agent wants to join a customer chat
|
|
||||||
// session. It can be used to connect to a specific session (by
|
|
||||||
// ID), or to just accept the session for which the customer has
|
|
||||||
// been waiting the longest (e.g. poll a FIFO queue of sessions
|
|
||||||
// awaiting a support agent). It is possible for multiple agents
|
|
||||||
// to be connected to the same chat session.
|
|
||||||
AcceptChat accept = 1;
|
|
||||||
// msg is used to send a message to the customer. It will also be
|
|
||||||
// delivered to any other connected support agents.
|
|
||||||
string msg = 2;
|
|
||||||
// leave_session allows an agent to exit a chat session. They can
|
|
||||||
// always re-enter later by sending an accept message for that
|
|
||||||
// session ID.
|
|
||||||
Void leave_session = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message AcceptChat {
|
|
||||||
string session_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatEntry {
|
|
||||||
google.protobuf.Timestamp date = 1;
|
|
||||||
oneof entry {
|
|
||||||
string customer_msg = 2;
|
|
||||||
AgentMessage agent_msg = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message ChatAgentResponse {
|
|
||||||
oneof resp {
|
|
||||||
// accepted_session provides the detail of a chat session. The server
|
|
||||||
// sends this message after the agent has accepted a chat session.
|
|
||||||
Session accepted_session = 1;
|
|
||||||
// msg is sent by the server when the customer, or another support
|
|
||||||
// agent, sends a message in stream's current session.
|
|
||||||
ChatEntry msg = 2;
|
|
||||||
// session_ended notifies the support agent that their currently
|
|
||||||
// connected chat session has been terminated by the customer.
|
|
||||||
Void session_ended = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message Session {
|
|
||||||
string session_id = 1;
|
|
||||||
string customer_name = 2;
|
|
||||||
repeated ChatEntry history = 3;
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/golang/protobuf/jsonpb"
|
"github.com/golang/protobuf/jsonpb"
|
||||||
|
|
@ -21,20 +22,111 @@ type accounts struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *accounts) openAccount(customer string, accountType Account_Type, initialBalanceCents int32) *Account {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
accountNums, ok := a.AccountNumbersByCustomer[customer]
|
||||||
|
if !ok {
|
||||||
|
// no accounts for this customer? it's a new customer
|
||||||
|
a.Customers = append(a.Customers, customer)
|
||||||
|
}
|
||||||
|
num := a.LastAccountNum + 1
|
||||||
|
a.LastAccountNum = num
|
||||||
|
a.AccountNumbers = append(a.AccountNumbers, num)
|
||||||
|
accountNums = append(accountNums, num)
|
||||||
|
a.AccountNumbersByCustomer[customer] = accountNums
|
||||||
|
var acct account
|
||||||
|
acct.AccountNumber = num
|
||||||
|
acct.BalanceCents = initialBalanceCents
|
||||||
|
acct.Transactions = append(acct.Transactions, &Transaction{
|
||||||
|
AccountNumber: num,
|
||||||
|
SeqNumber: 1,
|
||||||
|
Date: ptypes.TimestampNow(),
|
||||||
|
AmountCents: initialBalanceCents,
|
||||||
|
Desc: "initial deposit",
|
||||||
|
})
|
||||||
|
a.AccountsByNumber[num] = &acct
|
||||||
|
return &acct.Account
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *accounts) closeAccount(customer string, accountNumber uint64) error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
acctNums := a.AccountNumbersByCustomer[customer]
|
||||||
|
found := -1
|
||||||
|
for i, num := range acctNums {
|
||||||
|
if num == accountNumber {
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == -1 {
|
||||||
|
return status.Errorf(codes.NotFound, "you have no account numbered %d", accountNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
acct := a.AccountsByNumber[accountNumber]
|
||||||
|
if acct.BalanceCents != 0 {
|
||||||
|
return status.Errorf(codes.FailedPrecondition, "account %d cannot be closed because it has a non-zero balance: %s", accountNumber, dollars(acct.BalanceCents))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, num := range a.AccountNumbers {
|
||||||
|
if num == accountNumber {
|
||||||
|
a.AccountNumbers = append(a.AccountNumbers[:i], a.AccountNumbers[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.AccountNumbersByCustomer[customer] = append(acctNums[:found], acctNums[found+1:]...)
|
||||||
|
delete(a.AccountsByNumber, accountNumber)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *accounts) getAccount(customer string, accountNumber uint64) (*account, error) {
|
||||||
|
a.mu.RLock()
|
||||||
|
defer a.mu.RUnlock()
|
||||||
|
acctNums := a.AccountNumbersByCustomer[customer]
|
||||||
|
for _, num := range acctNums {
|
||||||
|
if num == accountNumber {
|
||||||
|
return a.AccountsByNumber[num], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.NotFound, "you have no account numbered %d", accountNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *accounts) getAllAccounts(customer string) []*Account {
|
||||||
|
a.mu.RLock()
|
||||||
|
defer a.mu.RUnlock()
|
||||||
|
|
||||||
|
accountNums := a.AccountNumbersByCustomer[customer]
|
||||||
|
var accounts []*Account
|
||||||
|
for _, num := range accountNums {
|
||||||
|
accounts = append(accounts, &a.AccountsByNumber[num].Account)
|
||||||
|
}
|
||||||
|
return accounts
|
||||||
|
}
|
||||||
|
|
||||||
type account struct {
|
type account struct {
|
||||||
Account
|
Account
|
||||||
Transactions []*Transaction
|
Transactions []*Transaction
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *account) newTransaction(amountCents int32, desc string) (int32, error) {
|
func (a *account) getTransactions() []*Transaction {
|
||||||
|
a.mu.RLock()
|
||||||
|
defer a.mu.RUnlock()
|
||||||
|
return a.Transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *account) newTransaction(amountCents int32, desc string) (newBalance int32, err error) {
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
newBalance := a.BalanceCents + amountCents
|
bal := a.BalanceCents + amountCents
|
||||||
if newBalance < 0 {
|
if bal < 0 {
|
||||||
return 0, status.Errorf(codes.FailedPrecondition, "insufficient funds: cannot withdraw %s when balance is %s", dollars(amountCents), dollars(a.BalanceCents))
|
return 0, status.Errorf(codes.FailedPrecondition, "insufficient funds: cannot withdraw %s when balance is %s", dollars(amountCents), dollars(a.BalanceCents))
|
||||||
}
|
}
|
||||||
a.BalanceCents += amountCents
|
a.BalanceCents = bal
|
||||||
a.Transactions = append(a.Transactions, &Transaction{
|
a.Transactions = append(a.Transactions, &Transaction{
|
||||||
AccountNumber: a.AccountNumber,
|
AccountNumber: a.AccountNumber,
|
||||||
Date: ptypes.TimestampNow(),
|
Date: ptypes.TimestampNow(),
|
||||||
|
|
@ -42,7 +134,7 @@ func (a *account) newTransaction(amountCents int32, desc string) (int32, error)
|
||||||
SeqNumber: uint64(len(a.Transactions) + 1),
|
SeqNumber: uint64(len(a.Transactions) + 1),
|
||||||
Desc: desc,
|
Desc: desc,
|
||||||
})
|
})
|
||||||
return a.BalanceCents, nil
|
return bal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) MarshalJSON() ([]byte, error) {
|
func (t *Transaction) MarshalJSON() ([]byte, error) {
|
||||||
|
|
@ -58,7 +150,7 @@ func (t *Transaction) UnmarshalJSON(b []byte) error {
|
||||||
return jsonpb.Unmarshal(bytes.NewReader(b), t)
|
return jsonpb.Unmarshal(bytes.NewReader(b), t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accounts) Clone() *accounts {
|
func (a *accounts) clone() *accounts {
|
||||||
var clone accounts
|
var clone accounts
|
||||||
clone.AccountNumbersByCustomer = map[string][]uint64{}
|
clone.AccountNumbersByCustomer = map[string][]uint64{}
|
||||||
clone.AccountsByNumber = map[uint64]*account{}
|
clone.AccountsByNumber = map[uint64]*account{}
|
||||||
|
|
@ -91,3 +183,7 @@ func (a *accounts) Clone() *accounts {
|
||||||
|
|
||||||
return &clone
|
return &clone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dollars(amountCents int32) string {
|
||||||
|
return fmt.Sprintf("$%02f", float64(amountCents)/100)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
//go:generate protoc --go_out=plugins=grpc:./ bank.proto
|
//go:generate protoc --go_out=plugins=grpc:./ bank.proto support.proto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -9,8 +9,9 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"os/signal"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
@ -67,6 +68,16 @@ func main() {
|
||||||
s.flush()
|
s.flush()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// trap SIGINT / SIGTERM to exit cleanly
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGINT)
|
||||||
|
signal.Notify(c, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
fmt.Println("Shutting down...")
|
||||||
|
grpcSvr.GracefulStop()
|
||||||
|
}()
|
||||||
|
|
||||||
grpclog.Infof("server starting, listening on %v", l.Addr())
|
grpclog.Infof("server starting, listening on %v", l.Addr())
|
||||||
if err := grpcSvr.Serve(l); err != nil {
|
if err := grpcSvr.Serve(l); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -112,11 +123,9 @@ func gRPCServer() *grpc.Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
type svr struct {
|
type svr struct {
|
||||||
datafile string
|
datafile string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
allAccounts accounts
|
allAccounts accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,10 +158,9 @@ func (s *svr) bgSaver() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *svr) flush() {
|
func (s *svr) flush() {
|
||||||
s.mu.Lock()
|
accounts := s.allAccounts.clone()
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if b, err := json.Marshal(&s.allAccounts); err != nil {
|
if b, err := json.Marshal(accounts); err != nil {
|
||||||
grpclog.Errorf("failed to save data to %q", s.datafile)
|
grpclog.Errorf("failed to save data to %q", s.datafile)
|
||||||
} else if err := ioutil.WriteFile(s.datafile, b, 0666); err != nil {
|
} else if err := ioutil.WriteFile(s.datafile, b, 0666); err != nil {
|
||||||
grpclog.Errorf("failed to save data to %q", s.datafile)
|
grpclog.Errorf("failed to save data to %q", s.datafile)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,118 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option go_package = "main";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// Support provides an interactive chat service, for customers to interact with
|
||||||
|
// the bank's support agents. A single stream, for either of the two methods, is
|
||||||
|
// a stateful connection to a single "chat session". Streams are initially disconnected
|
||||||
|
// (not part of any session). A stream must be disconnected from a session (via customer
|
||||||
|
// hang up or via agent leaving a session) before it can be connected to a new one.
|
||||||
|
service Support {
|
||||||
|
// ChatCustomer is used by a customer-facing app to send the customer's messages
|
||||||
|
// to a chat session. The customer is how initiates and terminates (via "hangup")
|
||||||
|
// a chat session. Only customers may invoke this method (e.g. requests must
|
||||||
|
// include customer auth credentials).
|
||||||
|
rpc ChatCustomer(stream ChatCustomerRequest) returns (stream ChatCustomerResponse);
|
||||||
|
// ChatAgent is used by an agent-facing app to allow an agent to reply to a
|
||||||
|
// customer's messages in a chat session. The agent may accept a chat session,
|
||||||
|
// which defaults to the session awaiting an agent for the longest period of time
|
||||||
|
// (FIFO queue).
|
||||||
|
rpc ChatAgent(stream ChatAgentRequest) returns (stream ChatAgentResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Void {
|
||||||
|
VOID = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatCustomerRequest {
|
||||||
|
oneof req {
|
||||||
|
// init is used when a chat stream is not part of a
|
||||||
|
// chat session. This is a stream's initial state, as well as
|
||||||
|
// the state after a "hang_up" request is sent. This creates
|
||||||
|
// a new state session or resumes an existing one.
|
||||||
|
InitiateChat init = 1;
|
||||||
|
// msg is used to send the customer's messages to support
|
||||||
|
// agents.
|
||||||
|
string msg = 2;
|
||||||
|
// hang_up is used to terminate a chat session. If a stream
|
||||||
|
// is broken, but the session was not terminated, the client
|
||||||
|
// may initiate a new stream and use init to resume that
|
||||||
|
// session. Sessions are not terminated unless done so
|
||||||
|
// explicitly via sending this kind of request on the stream.
|
||||||
|
Void hang_up = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiateChat {
|
||||||
|
string resume_session_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AgentMessage {
|
||||||
|
string agent_name = 1;
|
||||||
|
string msg = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatCustomerResponse {
|
||||||
|
oneof resp {
|
||||||
|
// session is sent from the server when the stream is connected
|
||||||
|
// to a chat session. This happens after an init request is sent
|
||||||
|
// and the stream is connected to either a new or resumed session.
|
||||||
|
Session session = 1;
|
||||||
|
// msg is sent from the server to convey agents' messages to the
|
||||||
|
// customer.
|
||||||
|
AgentMessage msg = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatAgentRequest {
|
||||||
|
oneof req {
|
||||||
|
// accept is used when an agent wants to join a customer chat
|
||||||
|
// session. It can be used to connect to a specific session (by
|
||||||
|
// ID), or to just accept the session for which the customer has
|
||||||
|
// been waiting the longest (e.g. poll a FIFO queue of sessions
|
||||||
|
// awaiting a support agent). It is possible for multiple agents
|
||||||
|
// to be connected to the same chat session.
|
||||||
|
AcceptChat accept = 1;
|
||||||
|
// msg is used to send a message to the customer. It will also be
|
||||||
|
// delivered to any other connected support agents.
|
||||||
|
string msg = 2;
|
||||||
|
// leave_session allows an agent to exit a chat session. They can
|
||||||
|
// always re-enter later by sending an accept message for that
|
||||||
|
// session ID.
|
||||||
|
Void leave_session = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message AcceptChat {
|
||||||
|
string session_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatEntry {
|
||||||
|
google.protobuf.Timestamp date = 1;
|
||||||
|
oneof entry {
|
||||||
|
string customer_msg = 2;
|
||||||
|
AgentMessage agent_msg = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatAgentResponse {
|
||||||
|
oneof resp {
|
||||||
|
// accepted_session provides the detail of a chat session. The server
|
||||||
|
// sends this message after the agent has accepted a chat session.
|
||||||
|
Session accepted_session = 1;
|
||||||
|
// msg is sent by the server when the customer, or another support
|
||||||
|
// agent, sends a message in stream's current session.
|
||||||
|
ChatEntry msg = 2;
|
||||||
|
// session_ended notifies the support agent that their currently
|
||||||
|
// connected chat session has been terminated by the customer.
|
||||||
|
Void session_ended = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Session {
|
||||||
|
string session_id = 1;
|
||||||
|
string customer_name = 2;
|
||||||
|
repeated ChatEntry history = 3;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue