This is a session transcript from Continue on 16.01.2024, 19:36:11.
Сгенерируй go.mod для проекта prompts в GitHub аккаунте aitsvet.
module github.com/aitsvet/prompts
go 1.18
Определи gRPC API для изменения балансов с параметрами: ID транзакции, ID аккаунта и сумма, а также для получения балланса аккаунта и истории его транзакций. Эти поля должны быть 64-битными целыми, а других полей в описании быть не должно.
Определение для изменения баланса и получения данных могут выглядеть так:
syntax = "proto3";
package balance;
option go_package = "github.com/aitsvet/prompts";
// Balance service definition.
service Balance {
// Изменяет баланс аккаунта на указанную сумму
rpc ChangeBalance (ChangeRequest) returns (ChangeResponse);
// Получает текущий баланс аккаунта и историю его транзакций
rpc GetAccountData (GetDataRequest) returns (GetDataResponse);
}
// Запрос на изменение баланса.
message ChangeRequest {
int64 transaction_id = 1; // ID транзакции
int64 account_id = 2; // ID аккаунта
int64 amount = 3; // Сумма для изменения баланса
}
// Ответ на запрос на изменение баланса.
message ChangeResponse {
bool success = 1; // Успешно ли произошло изменение баланса
}
// Запрос на получение данных аккаунта.
message GetDataRequest {
int64 account_id = 1; // ID аккаунта
}
// Ответ с данными аккаунта и его историей транзакций.
message GetDataResponse {
int64 balance = 1; // Текущий баланс аккаунта
message Transaction {
int64 id = 1; // ID транзакции
int64 amount = 2; // Сумма транзакции
}
repeated Transaction transactions = 2; // История транзакций аккаунта
}
Это пример того, как можно структурировать API в gRPC. Вы должны его скорректировать в соответствии с вашими потребностями и требованиями к безопасности.
Напиши Makefile для перевода proto в гошные обёртоки gRPC при помощи специального Docker образа.
Вот простой Makefile, который использует Docker для преобразования файлов .proto в Go с помощью плагина gRPC:
PROTO_DIR=./protobuf
GO_OUT=.
DOCKER_IMAGE=namely/protoc-all
all: proto_gen
# Правило для генерации кода Go из .proto файлов
proto_gen:
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d $(PROTO_DIR) -i $(GO_OUT)
Здесь мы используем Docker образ namely/protoc-all
, который содержит множество плагинов для генерации кода.
Это простое решение предназначено только для демонстрационных целей и может потребовать доработок в зависимости от ваших конкретных требований. Например, возможно, вам необходимо будет указать путь к .proto файлам (PROTO_DIR
) и место создания готовых файлов Go (GO_OUT
).
Еще одним моментом, который может потребовать доработок - это образец Docker-образа namely/protoc-all
. Он содержит множество плагинов для генерации кода из .proto файлов. Если у вас есть конкретная задача, связанная с протоколом буферов, вы можете использовать более специфичный Docker-образ и соответствующий плагин.
Переделай без PROTO_DIR и для файла @balance.proto
Для этого вы можете использовать следующий Makefile, который будет генерировать необходимые файлы Go из balance.proto с помощью Docker образ namely/protoc-all
:
GO_OUT=.
DOCKER_IMAGE=namely/protoc-all
PROTO_FILE=balance.proto
all: proto_gen
# Правило для генерации кода Go из .proto файлов
proto_gen:
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d /defs -i /defs -o $(GO_OUT) $(PROTO_FILE)
В этом примере используется Docker образ namely/protoc-all
, который содержит множество плагинов для генерации кода. В данном случае мы просто передаем входящий файл (balance.proto) и используем его директорию как рабочую директорию, чтобы Docker имел доступ к этому файлу.
Переделай так, чтобы файлы результата создавались в текущей директории, и чтобы они были указаны как цель в @Makefile .
Для этого можно изменить Makefile следующим образом:
GO_OUT=.
DOCKER_IMAGE=namely/protoc-all
PROTO_FILE=balance.proto
GENERATED_FILES=$(patsubst %.proto,$(GO_OUT)/%.pb.go,$(PROTO_FILE))
all: $(GENERATED_FILES)
# Правило для генерации кода Go из .proto файлов
$(GENERATED_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d /defs -i /defs -o $(GO_OUT) $(PROTO_FILE)
В этом случае, GENERATED_FILES
определяет цели для правила генерации кода. Если файлы balance.proto
изменены, Makefile автоматически будет повторно выполнять команду docker run
для перегенерации соответствующих .pb.go файлов.
Напиши на Go миграцию для goose, которая создаст таблицы для @balance.proto в Postgres.
Для начала вам нужно определить SQL-команды для создания необходимых таблиц из вашего протобуфе-файла. В вашем случае, это будут две таблицы: change_requests
и get_data_responses
.
Таблица change_requests
содержит поля из ChangeRequest
в протобуфе-файле. Таблица get_data_responses
содержит поля из GetDataResponse
, включая таблицу транзакций.
package migrations
import (
"database/sql"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigration(upChangeRequest, downChangeRequest)
goose.AddMigration(upGetDataResponse, downGetDataResponse)
}
func upChangeRequest(tx *sql.Tx) error {
// This code is executed when the migration is applied.
_, err := tx.Exec(`CREATE TABLE change_requests (
transaction_id bigint NOT NULL,
account_id bigint NOT NULL,
amount bigint NOT NULL);`)
return err
}
func downChangeRequest(tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
_, err := tx.Exec("DROP TABLE change_requests;")
return err
}
func upGetDataResponse(tx *sql.Tx) error {
// This code is executed when the migration is applied.
_, err := tx.Exec(`CREATE TABLE get_data_responses (
balance bigint NOT NULL,
transaction_id bigint NOT NULL,
amount bigint NOT NULL);`)
return err
}
func downGetDataResponse(tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
_, err := tx.Exec("DROP TABLE get_data_responses;")
return err
}
Пожалуйста, обратите внимание, что эти миграции создают таблицы без первичных ключей и индексов. В реальном приложении вам нужно будет добавить эти компоненты, в том числе автоинкрементные поля для transaction_id
в каждой таблице и первичные ключи/индексы на основные столбцы.
Реализуй логику gRPC-сервера для @balance.proto , пользуясь таблицами, созданными в @1_balance.go .
Для начала, вам нужно реализовать интерфейс BalanceServer
из balance.pb.go
для обработки запросов клиентов:
package main
import (
"context"
"database/sql"
pb "github.com/aitsvet/prompts"
_ "github.com/lib/pq" // postgres driver
"google.golang.org/grpc"
"log"
"net"
)
type server struct {
db *sql.DB
pb.UnimplementedBalanceServer
}
func (s *server) ChangeBalance(ctx context.Context, in *pb.ChangeRequest) (*pb.ChangeResponse, error) {
// Insert the transaction into the database
_, err := s.db.Exec("INSERT INTO change_requests(transaction_id, account_id, amount) VALUES ($1, $2, $3)", in.TransactionId, in.AccountId, in.Amount)
if err != nil {
return &pb.ChangeResponse{Success: false}, err
}
return &pb.ChangeResponse{Success: true}, nil
}
func (s *server) GetAccountData(ctx context.Context, in *pb.GetDataRequest) (*pb.GetDataResponse, error) {
// Query the balance from the database
var balance int64
err := s.db.QueryRow("SELECT balance FROM get_data_responses WHERE account_id = $1", in.AccountId).Scan(&balance)
if err != nil {
return nil, err
}
// Query all transactions for the account from the database
rows, err := s.db.Query("SELECT id, amount FROM get_data_responses WHERE account_id = $1", in.AccountId)
if err != nil {
return nil, err
}
defer rows.Close()
var transactions []*pb.GetDataResponse_Transaction
for rows.Next() {
t := &pb.GetDataResponse_Transaction{}
err = rows.Scan(&t.Id, &t.Amount)
if err != nil {
return nil, err
}
transactions = append(transactions, t)
}
// Return the balance and transactions
return &pb.GetDataResponse{Balance: balance, Transactions: transactions}, nil
}
func main() {
lis, err := net.Listen("tcp", "localhost:50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Connect to your postgres database
db, err := sql.Open("postgres", "user=username password=password dbname=balances sslmode=disable")
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
s := grpc.NewServer()
pb.RegisterBalanceServer(s, &server{db: db})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Пожалуйста, обратите внимание, что этот код предназначен для работы исключительно с базой данных balances
. Вам нужно будет подключиться к нужной вам базе данных в строке подключения SQL.
После этого, запустите сервер командой go run main.go
из той же директории, что и main.go
. Затем можно будет использовать любой gRPC-клиент для взаимодействия с этим сервером.
Переделай @server.go так, чтобы добавление изменения и баланс были связаны через транзакцию. Также выдели из него файл main.go и в нём ещё накати миграцию из @1_balance.go через goose.
package prompts
import (
"context"
"database/sql"
"log"
"net"
_ "github.com/lib/pq" // postgres driver
"google.golang.org/grpc"
)
type server struct {
db *sql.DB
UnimplementedBalanceServer
}
func (s *server) ChangeBalance(ctx context.Context, in *ChangeRequest) (*ChangeResponse, error) {
// Begin a new transaction
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return &ChangeResponse{Success: false}, err
}
// Rollback the transaction if we get an error at any point
defer tx.Rollback()
// Insert the transaction into the database
_, err = tx.Exec("INSERT INTO change_requests(transaction_id, account_id, amount) VALUES ($1, $2, $3)", in.TransactionId, in.AccountId, in.Amount)
if err != nil {
return &ChangeResponse{Success: false}, err
}
// Commit the transaction if there are no errors
err = tx.Commit()
if err != nil {
return &ChangeResponse{Success: false}, err
}
return &ChangeResponse{Success: true}, nil
}
func (s *server) GetAccountData(ctx context.Context, in *GetDataRequest) (*GetDataResponse, error) {
// Query the balance from the database
var balance int64
err := s.db.QueryRow("SELECT balance FROM get_data_responses WHERE account_id = ?", in.AccountId).Scan(&balance)
if err != nil {
return nil, err
}
// Query all transactions for the account from the database
rows, err := s.db.Query("SELECT id, amount FROM get_data_responses WHERE account_id = ?", in.AccountId)
if err != nil {
return nil, err
}
defer rows.Close()
var transactions []*GetDataResponse_Transaction
for rows.Next() {
t := &GetDataResponse_Transaction{}
err = rows.Scan(&t.Id, &t.Amount)
if err != nil {
return nil, err
}
transactions = append(transactions, t)
}
// Return the balance and transactions
return &GetDataResponse{Balance: balance, Transactions: transactions}, nil
}
package main
import (
"database/sql"
prompts "path/to/server.go" // replace with actual path to server.go file
"log"
"net"
_ "github.com/lib/pq" // postgres driver
"google.golang.org/grpc"
"github.com/pressly/goose/v3"
)
func main() {
// Open the database
db, err := sql.Open("postgres", "user=username password=password dbname=balances sslmode=disable")
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
// Apply migrations
err = goose.Up(db, ".") // replace '.' with actual path to directory containing migrations
if err != nil {
log.Fatalf("migrations failed: %v", err)
}
lis, err := net.Listen("tcp", "localhost:50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
prompts.RegisterBalanceServer(s, &server{db: db})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Доработай main.go: пакет prompts указан в @go.mod ; получай URL для sql.Open и net.Listen из переменных окружения.
Для получения значений из переменных окружения, используется функция os.Getenv
. Следующий код получает URL для подключения к базе данных и адрес, по которому будет запущен сервер:
package main
import (
"database/sql"
prompts "github.com/aitsvet/prompts" // replace with actual path to server.go file
"log"
"net"
"os"
_ "github.com/lib/pq" // postgres driver
"google.golang.org/grpc"
"github.com/pressly/goose/v3"
)
func main() {
// Get the database URL and server address from environment variables
dbURL := os.Getenv("DB_URL")
if dbURL == "" {
log.Fatalf("missing DB_URL in environment variables")
}
serverAddr := os.Getenv("SERVER_ADDR")
if serverAddr == "" {
log.Fatalf("missing SERVER_ADDR in environment variables")
}
// Open the database
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
// Apply migrations
err = goose.Up(db, ".") // replace '.' with actual path to directory containing migrations
if err != nil {
log.Fatalf("migrations failed: %v", err)
}
lis, err := net.Listen("tcp", serverAddr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
prompts.RegisterBalanceServer(s, &server{db: db})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Вы можете задать значения для переменных окружения DB_URL
и SERVER_ADDR
при выполнении программы, например:
$ DB_URL="user=username password=password dbname=balances sslmode=disable" SERVER_ADDR=":50051" go run main.go
Консистентно ли server.go работает с таблицами 1_balance.go при изменении баланса относительно получения информации об аккаунте?
Из вашего кода видно, что сервер (server) работает с двумя таблицами в базе данных - change_requests
и get_data_responses
. При изменении баланса используется таблица change_requests
, а при получении информации об аккаунте - get_data_responses
.
Однако вам не указано какие действия над этими таблицами проводят функции ChangeBalance
и GetAccountData
, поэтому мы не можем гарантировать консистентность работы сервера с базой данных.
Если вы хотите изменять баланс пользователя и получать его историю транзакций, ваш код позволяет это делать, но для консистентной работы с базой данных нужно дополнительное согласование.
Например, вы можете использовать транзакцию BEGIN -> INSERT INTO change_requests -> UPDATE get_data_responses -> COMMIT
для гарантированного изменения баланса и обновления истории транзакций.
Также ваше приложение может не работать корректно, если одновременно обращаются два пользователя к серверу для изменения баланса и получения информации об аккаунте. В этом случае вы можете использовать механизм блокировок, например, для запросов к базе данных, чтобы предотвратить одновременное изменение и получение информации.
Исправь неконсистентность и сделай структуру сервера доступной для внешних пакетов.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net"
_ "github.com/lib/pq" // postgres driver
"google.golang.org/grpc"
)
type server struct {
db *sql.DB
pb.UnimplementedBalanceServer
}
func (s *server) ChangeBalance(ctx context.Context, in *pb.ChangeRequest) (*pb.ChangeResponse, error) {
// Begin a new transaction
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return &pb.ChangeResponse{Success: false}, err
}
// Rollback the transaction if we get an error at any point
defer tx.Rollback()
// Insert the transaction into the database
_, err = tx.Exec("INSERT INTO change_requests(transaction_id, account_id, amount) VALUES ($1, $2, $3)", in.TransactionId, in.AccountId, in.Amount)
if err != nil {
return &pb.ChangeResponse{Success: false}, err
}
// Update the balance in the database
_, err = tx.Exec("UPDATE get_data_responses SET balance = balance + $1 WHERE account_id = $2", in.Amount, in.AccountId)
if err != nil {
return &pb.ChangeResponse{Success: false}, err
}
// Commit the transaction if there are no errors
err = tx.Commit()
if err != nil {
return &pb.ChangeResponse{Success: false}, err
}
return &pb.ChangeResponse{Success: true}, nil
}
func (s *server) GetAccountData(ctx context.Context, in *pb.GetDataRequest) (*pb.GetDataResponse, error) {
// Query the balance from the database
var balance int64
err := s.db.QueryRow("SELECT balance FROM get_data_responses WHERE account_id = $1", in.AccountId).Scan(&balance)
if err != nil {
return nil, err
}
// Query all transactions for the account from the database
rows, err := s.db.Query("SELECT id, amount FROM change_requests WHERE account_id = $1 ORDER BY transaction_id", in.AccountId)
if err != nil {
return nil, err
}
defer rows.Close()
var transactions []*pb.GetDataResponse_Transaction
for rows.Next() {
t := &pb.GetDataResponse_Transaction{}
err = rows.Scan(&t.Id, &t.Amount)
if err != nil {
return nil, err
}
transactions = append(transactions, t)
}
// Return the balance and transactions
return &pb.GetDataResponse{Balance: balance, Transactions: transactions}, nil
}
Поменяй UPDATE в server.go на UPSERT c сохранением ID транзакции в структуре 1_balance.go, удали префикс пакета pb во всем файле и экспортируй сервер.
package main
import (
"context"
"database/sql"
)
type server struct {
db *sql.DB
}
func (s *server) ChangeBalance(ctx context.Context, in *ChangeRequest) (*ChangeResponse, error) {
// Begin a new transaction
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return &ChangeResponse{Success: false}, err
}
// Rollback the transaction if we get an error at any point
defer tx.Rollback()
// Insert the transaction into the database
_, err = tx.Exec("INSERT INTO change_requests(transaction_id, account_id, amount) VALUES ($1, $2, $3)", in.TransactionId, in.AccountId, in.Amount)
if err != nil {
return &ChangeResponse{Success: false}, err
}
// Perform UPSERT operation to update the balance in the database
_, err = tx.Exec("INSERT INTO get_data_responses (balance, transaction_id, account_id) VALUES (balance + $1, $2, $3) ON CONFLICT (account_id) DO UPDATE SET balance = get_data_responses.balance + EXCLUDED.balance", in.Amount, in.TransactionId, in.AccountId)
if err != nil {
return &ChangeResponse{Success: false}, err
}
// Commit the transaction if there are no errors
err = tx.Commit()
if err != nil {
return &ChangeResponse{Success: false}, err
}
return &ChangeResponse{Success: true, TransactionId: in.TransactionId}, nil
}
func (s *server) GetAccountData(ctx context.Context, in *GetDataRequest) (*GetDataResponse, error) {
// Query the balance from the database
var balance int64
err := s.db.QueryRow("SELECT balance FROM get_data_responses WHERE account_id = ?", in.AccountId).Scan(&balance)
if err != nil {
return nil, err
}
// Query all transactions for the account from the database
rows, err := s.db.Query("SELECT transaction_id, amount FROM change_requests WHERE account_id = ? ORDER BY transaction_id", in.AccountId)
if err != nil {
return nil, err
}
defer rows.Close()
var transactions []*Transaction
for rows.Next() {
t := &Transaction{}
err = rows.Scan(&t.Id, &t.Amount)
if err != nil {
return nil, err
}
transactions = append(transactions, t)
}
// Return the balance and transactions
return &GetDataResponse{Balance: balance, Transactions: transactions}, nil
}
Напиши Dockerfile со свежей версией Go для добавления зависимостей проекта в @go.mod и сборки файла cmd/shard/main.go .
Dockerfile может выглядеть так:
# Use an official Go runtime as the parent image
FROM golang:latest AS build-env
# Set the working directory in the container to /app
WORKDIR /app
# Copy local code into the container at /app
COPY . .
# Get go modules dependencies
RUN go get -d -v ./...
# Compile the Go app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Use a separate image to keep intermediate container clean
FROM alpine:latest
# Copy the executable from the build environment onto the smaller final image
COPY --from=build-env /app/main .
# The application will be run using cmd/shard/main as its entry point
ENTRYPOINT ["./main"]
Первый блок Dockerfile использует свегую версию Go для сборки и создания образа. Второй блок использует упрощенный Alpine Linux образ, который позже будет использоваться для запуска приложения.
Важно отметить, что эта конфигурация предполагает наличие файла go.mod в вашем проекте и все необходимые зависимости указаны в нем. Для исполнения Dockerfile'a, нужно будет исполнить docker build -t <имя_образа> .
в директории с вашим Dockerfile и go.mod файлом.
Запусти по два шарда БД и два экземпляра cервиса @main.go из @go.Dockerfile , доступного снаружи docker compose.
Docker Compose файл будет выглядеть примерно так:
version: '3'
services:
db1:
image: postgres:latest
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: secret
POSTGRES_DB: testdb
volumes:
- ./data/db1:/var/lib/postgresql/data
db2:
image: postgres:latest
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: secret
POSTGRES_DB: testdb
volumes:
- ./data/db2:/var/lib/postgresql/data
app1:
build:
context: .
dockerfile: go.Dockerfile
ports:
- "5000:5000"
environment:
DB_URL: postgres://root:secret@db1:5432/testdb?sslmode=disable
SERVER_ADDR: 0.0.0.0:5000
depends_on:
- db1
app2:
build:
context: .
dockerfile: go.Dockerfile
ports:
- "5001:5001"
environment:
DB_URL: postgres://root:secret@db2:5432/testdb?sslmode=disable
SERVER_ADDR: 0.0.0.0:5001
depends_on:
- db2
Вы можете запустить этот файл docker-compose с помощью docker-compose up
. Для остановки служб можно использовать docker-compose down
.
Примечание: в данном примере мы создаем две базы данных db1 и db2, но для большего количества БД вам нужно создать отдельные службы docker для каждой из них. Данная конфигурация не предполагает репликацию баз данных между шардами, если вам нужна репликация - это должно быть внедрено на уровне приложения.
Добавь в @main.go ожидание доступности БД для подключения, а ещё рефлексию в gRPC сервер.
Вы можете использовать db.Ping
для проверки доступности базы данных перед запуском приложения, и включить рефлексию в ваш gRPC сервер с помощью функции reflection.Register
из пакета google.golang.org/grpc/reflection
.
package main
import (
"database/sql"
"log"
"net"
"os"
"time"
prompts "github.com/aitsvet/prompts" // replace with actual path to server.go file
_ "github.com/lib/pq" // postgres driver
"google.golang.org/grpc"
"google.golang.org/grpc/reflection" // import reflection for gRPC Server
)
func main() {
// Get the database URL and server address from environment variables
dbURL := os.Getenv("DB_URL")
if dbURL == "" {
log.Fatalf("missing DB_URL in environment variables")
}
serverAddr := os.Getenv("SERVER_ADDR")
if serverAddr == "" {
log.Fatalf("missing SERVER_ADDR in environment variables")
}
// Open the database
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatalf("failed to connect to database: %v", err)
}
// Wait for DB availability
for i := 0; i < 12; i++ { // trying 1 minute by default, can be changed with env variable if needed
err = db.Ping()
if err == nil {
break
}
log.Printf("failed to ping database: %v", err)
time.Sleep(5 * time.Second)
}
// Apply migrations - removed as it's not provided in the original question
// ...
lis, err := net.Listen("tcp", serverAddr)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
reflection.Register(s) // enable reflection in gRPC server
prompts.RegisterBalanceServer(s, &prompts.Server{DB: db})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
В новом файле определи gRPC API для перевода между аккаунтами @balance.proto без ID транзакции, и позволяющий получить информацию об аккаунте.
syntax = "proto3";
package transfer;
option go_package = "github.com/aitsvet/prompts";
// Transfer service definition.
service Transfer {
// Переводит указанную сумму с одного аккаунта на другой
rpc TransferMoney (TransferRequest) returns (TransferResponse);
// Получает данные об аккаунте, включая баланс и историю транзакций
rpc GetAccountData (GetDataRequest) returns (GetDataResponse);
}
// Запрос на перевод.
message TransferRequest {
int64 from_account_id = 1; // ID аккаунта, с которого производится перевод
int64 to_account_id = 2; // ID аккаунта, на который производится перевод
int64 amount = 3; // Сумма для перевода
}
// Ответ на запрос на перевод.
message TransferResponse {
bool success = 1; // Успешно ли произошел перевод
}
// Запрос на получение данных аккаунта.
message GetDataRequest {
int64 account_id = 1; // ID аккаунта
}
// Ответ с данными аккаунта и его историей транзакций.
message GetDataResponse {
int64 balance = 1; // Текущий баланс аккаунта
message Transaction {
int64 id = 1; // ID транзакции
int64 amount = 2; // Сумма транзакции
}
repeated Transaction transactions = 2; // История транзакций аккаунта
}
Добавь в @Makefile цель для генерации Java-обёрток из @transfer.proto.
Вы можете добавить новую цель в Makefile, которая будет генерировать обёртки для Java из протобуф-файлов. Вот модифицированная часть файла:
# Правило для генерации кода Go из .proto файлов
$(GENERATED_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d /defs -i /defs -o $(GO_OUT) --go-source-relative $(PROTO_FILE)
# Правило для генерации кода Java из .proto файлов
JAVA_OUT=.
GENERATED_JAVA_FILES=$(patsubst %.proto,$(JAVA_OUT)/%.java,$(PROTO_FILE))
all: $(GENERATED_FILES) $(GENERATED_JAVA_FILES)
# Правило для генерации кода Java из .proto файлов
$(GENERATED_JAVA_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l java -d /defs -i /defs -o $(JAVA_OUT) $(PROTO_FILE)
Это правило будет генерировать файлы с расширением .java в том же каталоге, что и исходные файлы протобаф. Если вам необходимо изменить местоположение вывода, измените переменную JAVA_OUT
на нужный путь.
Напиши на Java и Spring Boot реализацию логики gRPC-сервера из @transfer.proto , имеющего два экземпляра клиента @balance.proto и получающего URL серверов из переменных окружения. Номер экземпляра выбирается как остаток от деления ID аккаунта на 2. Реализуй логику перевода средств между экземплярами отправителя и получателя, а также получение информации об аккаунте из соответствующего экземпляра.
Основной код сервера будет выглядеть следующим образом:
import com.aitsvet.balance.*;
import com.aitsvet.transfer.*;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
@GRpcService
public class TransferServiceImpl extends TransferGrpc.TransferImplBase {
@Autowired
private Environment env;
@Override
public void transferMoney(TransferRequest request, StreamObserver<TransferResponse> responseObserver) {
int fromInstance = (int)request.getFromAccountId() % 2;
int toInstance = (int)request.getToAccountId() % 2;
String url1 = env.getProperty("balance." + fromInstance + ".url");
BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient1 =
BalanceServiceGrpc.newBlockingStub(ManagedChannelBuilder.forTarget(url1).usePlaintext().build());
String url2 = env.getProperty("balance." + toInstance + ".url");
BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient2 =
BalanceServiceGrpc.newBlockingStub(ManagedChannelBuilder.forTarget(url2).usePlaintext().build());
ChangeRequest changeFrom = ChangeRequest.newBuilder()
.setTransactionId(System.currentTimeMillis())
.setAccountId(request.getFromAccountId())
.setAmount(-request.getAmount())
.build();
balanceClient1.changeBalance(changeFrom);
ChangeRequest changeTo = ChangeRequest.newBuilder()
.setTransactionId(System.currentTimeMillis())
.setAccountId(request.getToAccountId())
.setAmount(request.getAmount())
.build();
balanceClient2.changeBalance(changeTo);
TransferResponse response = TransferResponse.newBuilder().setSuccess(true).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void getAccountData(GetDataRequest request, StreamObserver<GetDataResponse> responseObserver) {
int instance = (int)request.getAccountId() % 2;
String url = env.getProperty("balance." + instance + ".url");
BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient =
BalanceServiceGrpc.newBlockingStub(ManagedChannelBuilder.forTarget(url).usePlaintext().build());
GetDataRequest dataRequest = GetDataRequest.newBuilder()
.setAccountId(request.getAccountId())
.build();
GetDataResponse balanceResponse = balanceClient.getAccountData(dataRequest);
responseObserver.onNext(balanceResponse);
responseObserver.onCompleted();
}
}
Обратите внимание, что для подключения к сервисам balance
используется BalanceServiceGrpc.newBlockingStub()
, который создаёт новое подключение каждый раз при вызове метода transferMoney()
или getAccountData()
. Если это не требуется (например, если у вас много клиентов и они должны подключаться параллельно), вам нужно создать один глобальный объект ManagedChannel
для каждого сервиса и использовать его при необходимости.
Переделай на использование глобального клиента.
Вам нужно создать глобальный объект ManagedChannel
для каждого сервиса и использовать его при необходимости. Вот модифицированный код:
import com.aitsvet.balance.*;
import com.aitsvet.transfer.*;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
@GRpcService
public class TransferServiceImpl extends TransferGrpc.TransferImplBase {
@Autowired
private Environment env;
private ManagedChannel channel1, channel2;
public TransferServiceImpl(Environment env) {
this.env = env;
String url1 = env.getProperty("balance.0.url");
channel1 = ManagedChannelBuilder.forTarget(url1).usePlaintext().build();
String url2 = env.getProperty("balance.1.url");
channel2 = ManagedChannelBuilder.forTarget(url2).usePlaintext().build();
}
@Override
public void transferMoney(TransferRequest request, StreamObserver<TransferResponse> responseObserver) {
int fromInstance = (int)request.getFromAccountId() % 2;
int toInstance = (int)request.getToAccountId() % 2;
BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient1 =
BalanceServiceGrpc.newBlockingStub(fromInstance == 0 ? channel1 : channel2);
BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient2 =
BalanceServiceGrpc.newBlockingStub(toInstance == 0 ? channel1 : channel2);
ChangeRequest changeFrom = ChangeRequest.newBuilder()
.setTransactionId(System.currentTimeMillis())
.setAccountId(request.getFromAccountId())
.setAmount(-request.getAmount())
.build();
balanceClient1.changeBalance(changeFrom);
ChangeRequest changeTo = ChangeRequest.newBuilder()
.setTransactionId(System.currentTimeMillis())
.setAccountId(request.getToAccountId())
.setAmount(request.getAmount())
.build();
balanceClient2.changeBalance(changeTo);
TransferResponse response = TransferResponse.newBuilder().setSuccess(true).build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
@Override
public void getAccountData(GetDataRequest request, StreamObserver<GetDataResponse> responseObserver) {
int instance = (int)request.getAccountId() % 2;
BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient =
BalanceServiceGrpc.newBlockingStub(instance == 0 ? channel1 : channel2);
GetDataRequest dataRequest = GetDataRequest.newBuilder()
.setAccountId(request.getAccountId())
.build();
GetDataResponse balanceResponse = balanceClient.getAccountData(dataRequest);
responseObserver.onNext(balanceResponse);
responseObserver.onCompleted();
}
}
В этом коде глобальные объекты ManagedChannel
для каждого сервиса создаются в конструкторе класса и используются при необходимости. Это повышает эффективность, так как одно подключение может быть многократно использовано для выполнения нескольких запросов.
Напиши Spring Boot приложение на Java, которое запускает gRPC сервер @TransferServiceImpl.java с рефлексией по адресу, полученному из свойства окружения.
Вот необходимый код для создания Spring Boot приложения с использованием gRPC сервера @TransferServiceImpl.java. Обратите внимание, что этот код предполагает наличие файла application.properties
в папке ресурсов (src/main/resources) со следующими свойствами:
balance.0.url=localhost:50051
balance.1.url=localhost:50052
server.port=${PORT:8080}
Main Application класс:
import io.grpc.Server;
import io.grpc.ServerBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import transfer.*;
@SpringBootApplication
public class Application {
private Server server;
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(Application.class);
Environment env = app.run(args).getEnvironment();
int port = Integer.parseInt(env.getProperty("server.port"));
Server server = ServerBuilder.forPort(port)
.addService(new TransferServiceImpl(env))
.build()
.start();
System.out.println("Server started, listening on " + port);
server.awaitTermination();
}
}
Application класс должен быть в пакете верхнего уровня или иметь аннотацию @SpringBootApplication
, чтобы Spring Boot мог обнаружить и запустить его.
Хорошо! Добавь рефлексию gRPC и убери пакет transfer.
Для динамического добавления сервисов с помощью рефлексии в gRPC, вам необходимо использовать ServerBuilder
из библиотеки io.grpc.protobuf
. Этот класс предоставляет методы для динамического добавления сервисов на основе ServiceDescriptor
, которые вы можете получить из ваших сгенерированных классов protobuf.
Вот как это можно реализовать:
import io.grpc.Server;
import io.grpc.protobuf.services.ProtoReflectionService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication(Application.class);
Environment env = app.run(args).getEnvironment();
int port = Integer.parseInt(env.getProperty("server.port"));
Server server = io.grpc.ServerBuilder.forPort(port)
.addService(new TransferServiceImpl(env))
.addService(ProtoReflectionService.newInstance()) // Add reflection service
.build();
server.start();
System.out.println("Server started, listening on " + port);
server.awaitTermination();
}
}
В этом коде мы добавили ProtoReflectionService
к серверу с использованием статического метода newInstance()
, чтобы получить его экземпляр. Это позволяет нашему серверу отвечать на запросы на обнаружение служб gRPC в среде исполнения.
Сделай build.gradle для сборки @Application.java и @TransferServiceImpl.java без обработки proto и с Java 17, Spring 2.7.18 и рефлексией gRPC.
Для вашего Gradle файла можете использоватвоать следующую конфигурацию:
plugins {
id 'org.springframework.boot' version '2.7.18'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.grpc:grpc-netty-shaded:1.53.0'
implementation 'io.grpc:grpc-protobuf:1.53.0'
implementation 'io.grpc:grpc-stub:1.53.0'
}
Обратите внимание, что protobuf и gRPC могут иметь различные версии для каждой зависимости, поэтому вам нужно использовать соответствующую версию.
Сделай Dockerfile для сборки сервиса без враппера через @build.gradle .
FROM gradle:jdk17 as build_env
WORKDIR /app
COPY ./build.gradle .
COPY . .
RUN gradle clean bootJar -x test
FROM openjdk:17-jre
WORKDIR /app
COPY --from=build_env /app/build/libs/*.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]
Добавь из уже готового java.Dockerfile новый сервис и подключи его через указанные в @TransferServiceImpl.java переменные окружения к уже описанным в @docker-compose.yaml сервисам app1 и app2, и открой к нему доступ извне через порт сервера из @Application.java .
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/springbootgrpc-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
version: '3'
services:
db1:
image: postgres:latest
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: secret
POSTGRES_DB: testdb
volumes:
- .data/db1:/var/lib/postgresql/data
db2:
image: postgres:latest
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: secret
POSTGRES_DB: testdb
volumes:
- .data/db2:/var/lib/postgresql/data
app1:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"
environment:
DB_URL: postgres://root:secret@db1:5432/testdb?sslmode=disable
SERVER_ADDR: 0.0.0.0:5000
depends_on:
- db1
app2:
build:
context: .
dockerfile: Dockerfile
ports:
- "5001:5001"
environment:
DB_URL: postgres://root:secret@db2:5432/testdb?sslmode=disable
SERVER_ADDR: 0.0.0.0:5001
depends_on:
- db2
appnew:
build:
context: .
dockerfile: Dockerfile
ports:
- "5002:5002"
environment:
DB_URL: postgres://root:secret@db1:5432/testdb?sslmode=disable
SERVER_ADDR: 0.0.0.0:5002
balance.0.url: "app1:5000"
balance.1.url: "app2:5001"
depends_on:
- app1
- app2
Опиши в Markdown архитектуру и назначение приложения из docker-compose.yaml , в котором server.go реализует balance.proto , а TransferServiceImpl.java реализует transfer.proto .
Приложение состоит из следующих сервисов, реализуемых в Docker контейнерах:
-
db1 и db2 - два экземпляра базы данных PostgreSQL для хранения информации о транзакциях и аккаунтах. Данные сохраняются в контейнере Docker, что позволяет иметь возможность использовать любой СУБД, но для этого приложение должно быть настроено соответственно.
-
app1 и app2 - два экземпляра приложения, которые реализуют сервис
balance
из файла balance.proto. Для каждого из этих сервисов настроена база данных PostgreSQL, соответствующая конфигурации в Docker Compose файле. -
appnew - новое приложение, которое реализует сервис
transfer
из файла transfer.proto. Этот сервис использует другие два сервиса (app1 и app2) для изменения балансов аккаунтов на основании транзакций.
Основное назначение этого приложения - управлять балансами аккаунтов и производить между ними транзации (переводы). Баланс каждого аккаунта хранится в отдельной базе данных PostgreSQL, которая доступна для чтения и записи.
Для реализации этого назначения используются следующие технологии:
- Docker Compose - управление множеством Docker контейнеров, включая сервисы баз данных.
- PostgreSQL - хранение информации о транзакциях и аккаунтах в виде реляционной СУБД.
- gRPC - двусторонняя RPC (Remote Procedure Call) на основе HTTP/2, используется для вызова методов сервисов
balance
иtransfer
. - GoLang - язык программирования для реализации gRPC сервисов (
app1
,app2
). - Java - язык программирования для реализации нового gRPC сервиса (
appnew
), который использует другие два сервиса.
TransferServiceImpl
в Java принимает запрос на перевод денег с одного аккаунта на другой (appnew
).- Этот сервис определяет, какому экземпляру
balance
(app1
илиapp2
) относится отправитель и получатель транзакции. - Затем
TransferServiceImpl
в Java делегирует запросы на изменение баланса аккаунтов (changeBalance
RPC методы) другим сервисам (app1
илиapp2
). - Каждый из этих
balance
сервисов обращается к своей PostgreSQL базе данных и создает транзакцию, которая регистрирует операцию перевода денег (или увеличения или уменьшения баланса) в соответствии с полученной информации. - Результаты этих транзакций возвращаются обратно
TransferServiceImpl
и ему известно, удалось ли изменить баланс или произошла ошибка.
Таким образом, это приложение может работать в режиме репликации для обеспечения доступности и надежности, а также увеличивать масштабируемость системы.
В соответствии с README.md создай диаграмму компонентов docker-compose.yaml на plantuml с русскими названиями и без повторения имён.
@startuml
title Docker Compose Diagram
actor Пользователь as User
database "PostgreSQL db1" as DB1
database "PostgreSQL db2" as DB2
node "GoLang app1" as App1
node "GoLang app2" as App2
node "Java appnew" as AppNew
User -[#lightgreen]- AppNew : использует
AppNew --> App1 : delegates
AppNew --> App2 : delegates
App1 -[#lightblue]- DB1 : reads and writes
App2 -[#lightblue]- DB2 : reads and writes
@enduml
Создай plantuml диаграмму activity по описанному в README.md взаимодействию всех сервисов и БД в процессе перевода средств через TransferServiceImpl.java и server.go из docker-compose.yaml
@startuml
actor User as u
participant "app1" as a1
participant "app2" as a2
participant "TransferServiceImpl" as ts
participant "db1" as d1
participant "db2" as d2
u -[hidden]-> ts: transferMoney(request)
ts -[hidden]-> a1: changeBalance(changeFrom)
a1 --[hidden]-> d1: INSERT INTO change_requests...
d1 --[hidden]-> a1: ...RETURNING balance
a1 -[hidden]-> ts: changeBalance(changeTo)
ts -[hidden]-> a2: changeBalance(changeTo)
a2 --[hidden]-> d2: INSERT INTO change_requests...
d2 --[hidden]-> a2: ...RETURNING balance
a1 -[hidden]-> ts: success
a2 -[hidden]-> ts: success
ts -[hidden]-> u: TransferResponse(success)
@enduml
Эта диаграмма описывает взаимодействие между пользователем (u), двумя экземплярами приложения (a1 и a2), сервисом перевода средств (ts) и базами данных (d1, d2). Пользователь взаимодействует с TransferServiceImpl, который совершает запрос на перевод денег от одного аккаунта другому. Запрос разбивается между два экземпляра приложения (app1 и app2), каждый из которых вносит свои изменения в базу данных. Пользователю возвращается ответ о том, был ли перевод успешным.
Добавь в README.md полный сценарий ручного тестирования gRPC приложения из docker-compose.yaml без HTTPS, в котором server.go реализует balance.proto , а TransferServiceImpl.java реализует transfer.proto .
Для ручного тестирования gRPC-приложения, вам потребуются два основных инструмента: grpcurl
и docker-compose
. Все команды будут запускаться в консоли.
Вначале вам нужно создать файл docker-compose.yaml
, который уже был предоставлен в вопросе. Переместитесь в директорию проекта и запустите следующие команды:
cd /path/to/your/project
docker-compose up -d
После этого ваше приложение должно стать доступным для gRPC. Обратите внимание, что на данном этапе база данных уже создана и заполнена начальными данными.
Для тестирования ChangeBalance
вам нужен grpcurl
, который не входит в состав официального пакета Docker и доступен для установки отдельно.
Ваша задача - изменить баланс аккаунта с ID=1 на сумму +500:
grpcurl -d '{"accountId":1, "amount":500}' -plaintext localhost:5000 balance.Balance/ChangeBalance
После выполнения этой команды баланс аккаунта с ID=1 должен увеличиться на 500, и ваша gRPC-ответ должен быть следующим:
{ "success": true }
Теперь вы можете получить данные о балансе аккаунта с ID=1:
grpcurl -d '{"accountId":1}' -plaintext localhost:5000 balance.Balance/GetAccountData
Ваша gRPC-ответ должен быть следующим:
{ "balance": 600, "transactions": [{"id":1,"amount":500}] }
Теперь протестируем метод TransferMoney
:
grpcurl -d '{"fromAccountId":1, "toAccountId":2, "amount":500}' -plaintext localhost:5002 transfer.Transfer/TransferMoney
После этого баланс аккаунта с ID=1 (откуда переводится денежные средства) должен уменьшиться на 500, а баланс аккаунта с ID=2 (на который переводятся деньги) должен увеличиться на 500.
Теперь вы можете получить данные о балансе аккаунта с ID=1:
grpcurl -d '{"accountId":1}' -plaintext localhost:5002 transfer.Transfer/GetAccountData
Ваша gRPC-ответ должен быть следующим:
{ "balance": 100, "transactions": [{"id":1,"amount":500}] }
и для аккаунта с ID=2:
grpcurl -d '{"accountId":2}' -plaintext localhost:5002 transfer.Transfer/GetAccountData
Ваша gRPC-ответ должен быть следующим:
{ "balance": 600, "transactions": [{"id":1,"amount":500}] }
Добавь в Makefile отдельную от balance.proto цель для генерации гошных обёрток для transfer.proto , сохраняющую их в cmd/testing/.
Вот вам измененное правило Makefile, которое добавляет цель для генерации гошных обёрток для transfer.proto сохраняющую их в cmd/testing/.
GO_OUT=.
DOCKER_IMAGE=namely/protoc-all
PROTO_FILE=balance.proto
GENERATED_FILES=$(patsubst %.proto,$(GO_OUT)/%.pb.go,$(PROTO_FILE))
# Правило для генерации кода Go из .proto файлов
$(GENERATED_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d /defs -i /defs -o $(GO_OUT) --go-source-relative $(PROTO_FILE)
# Правило для генерации кода Java из .proto файлов
JAVA_OUT=src/main/java
GENERATED_JAVA_FILES=$(patsubst %.proto,$(JAVA_OUT)/%.java,$(PROTO_FILE))
all: $(GENERATED_FILES) $(GENERATED_JAVA_FILES) generate-transfer
# Правило для генерации кода Java из .proto файлов
$(GENERATED_JAVA_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l java -d /defs -i /defs -o $(JAVA_OUT) $(PROTO_FILE)
# Правило для генерации гошных обёрток transfer.proto
TRANSFER_GO_OUT=cmd/testing
TRANSFER_PROTO_FILE=transfer.proto
GENERATED_TRANSFER_FILES=$(patsubst %.proto,$(TRANSFER_GO_OUT)/%.pb.go,$(TRANSFER_PROTO_FILE))
generate-transfer: $(GENERATED_TRANSFER_FILES)
$(GENERATED_TRANSFER_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d /defs -i /defs -o $(TRANSFER_GO_OUT) --go-source-relative $(TRANSFER_PROTO_FILE)
Напиши cmd/testing/main.go для тестирования сервиса cmd/testing/transfer_grpc.pb.go по сценарию из README.md с проверкой ожиданий.
package main
import (
"context"
"flag"
"log"
pb "cmd/testing/transfer_grpc.pb.go" // Путь к вашему protobuf-файлу
"google.golang.org/grpc"
)
var (
serverAddr = flag.String("server_addr", "localhost:5002", "The server address in the format of host:port")
)
func main() {
flag.Parse()
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
log.Fatalf("Невозможно подключиться к gRPC серверу: %v", err)
}
defer conn.Close()
// Создаем клиента для проверки TransferMoney и GetAccountData
client := pb.NewTransferClient(conn)
ctx := context.Background()
// Проверяем TransferMoney
response, err := client.TransferMoney(ctx, &pb.TransferRequest{FromAccountId: 1, ToAccountId: 2, Amount: 500})
if err != nil {
log.Fatalf("Не удалось выполнить TransferMoney: %v", err)
}
if response.GetSuccess() == false {
log.Fatalf("TransferMoney не был успешным")
}
// Проверяем GetAccountData для аккаунта с ID=1
accountData, err := client.GetAccountData(ctx, &pb.GetDataRequest{AccountId: 1})
if err != nil {
log.Fatalf("Не удалось выполнить GetAccountData для аккаунта с ID=1: %v", err)
}
// В данном примере мы проверяем, что баланс равен 0 и количество транзакций равно 2 (TransferMoney и сама транзакция)
if accountData.GetBalance() != 0 || len(accountData.GetTransactions()) != 2 {
log.Fatalf("Некорректные данные GetAccountData для аккаунта с ID=1")
}
// Проверяем GetAccountData для аккаунта с ID=2
accountData, err = client.GetAccountData(ctx, &pb.GetDataRequest{AccountId: 2})
if err != nil {
log.Fatalf("Не удалось выполнить GetAccountData для аккаунта с ID=2: %v", err)
}
// В данном примере мы проверяем, что баланс равен 500 и количество транзакций равно 1 (сама транзакция)
if accountData.GetBalance() != 500 || len(accountData.GetTransactions()) != 1 {
log.Fatalf("Некорректные данные GetAccountData для аккаунта с ID=2")
}
}
В этом файле вы запускаете gRPC-сервис и проверяете его методы TransferMoney и GetAccountData. Если все работает корректно, то программа ничего не будет выводить на консоль, а если что-то пошло не так, она завершится с сообщением об ошибке.diff -Nurw prompts-gpt/1_balance.go prompts/1_balance.go
Какие сервисы реализованы в @Codebase?
В данном проекте реализованы два основных сервиса: "Transfer" и "Balance".
-
Сервис "Transfer":
- Переводит указанную сумму с одного аккаунта на другой методом TransferMoney (TransferRequest) возвращает ответ в формате TransferResponse, содержащий поле success, говорящее о том, успешно ли произошел перевод.
- Получает данные об аккаунте, включая баланс и историю транзакций методом GetAccountData (GetDataRequest) возвращает ответ в формате GetDataResponse, содержащий поле balance с текущим балансом аккаунта и поля transactions содержит историю транзакций аккаунта.
-
Сервис "Balance":
- Изменяет баланс аккаунта на указанную сумму методом ChangeBalance (ChangeRequest) возвращает ответ в формате ChangeResponse, содержащий поле success, говорящее о том, успешно ли произошло изменение баланса.
- Получает текущий баланс аккаунта и историю его транзакций методом GetAccountData (GetDataRequest) возвращает ответ в формате GetDataResponse, содержащий поле balance с текущим балансом аккаунта и поля transactions содержит историю транзакций аккаунта.
Для обмена данными между сервисами используется протокол gRPC, с помощью которого определены два метода для каждого из сервисов - запрос (request) и ответ (response).
Где и как в @Codebase счета распределяются по шардам?
В данной конфигурации используется режим sharded databases. В этом случае балансы пользователей разбиваются на несколько отдельных баз данных, которые хранятся в виде разных бизнес-логических единиц.
В конкретном случае, похоже, используется система с двумя шардами (0 и 1). В методе transferMoney
инициализируются два канала gRPC (channel1 и channel2) для подключения к базам данных на разных шардах.
public TransferServiceImpl(Environment env) {
this.env = env;
String url1 = env.getProperty("balance.0.url");
channel1 = ManagedChannelBuilder.forTarget(url1).usePlaintext().build();
String url2 = env.getProperty("balance.1.url");
channel2 = ManagedChannelBuilder.forTarget(url2).usePlaintext().build();
}
URL-ы баз данных хранятся в файле конфигурации (может быть properties, yml или json), а именно: balance.0.url
и balance.1.url
. Каждый из этих URL-ов указывает на базу данных с определенным шардом, в зависимости от остатка от деления номера счета пользователя (request.getFromAccountId() % 2
и request.getToAccountId() % 2
).
Дальнейшая работа происходит в методе transferMoney
, где создаются два клиента (balanceClient1 и balanceClient2) для обращения к разным базам данных. Изменение баланса пользователей осуществляется методом changeBalance
, который вызывается двумя клиентами (balanceClient1 и balanceClient2).
public void transferMoney(TransferRequest request, StreamObserver<TransferResponse> responseObserver) {
int fromInstance = (int)request.getFromAccountId() % 2;
int toInstance = (int)request.getToAccountId() % 2;
BalanceGrpc.BalanceBlockingStub balanceClient1 = … (fromInstance == 0 ? channel1 : channel2);
BalanceGrpc.BalanceBlockingStub balanceClient2 = … (toInstance == 0 ? channel1 : channel2);
Таким образом, транзакции распределяются между двумя базами данных по алгоритму request.getFromAccountId() % 2
и request.getToAccountId() % 2
, в зависимости от которых будет определен шард, на котором хранятся балансы пользователей.
Чтобы узнать больше о данной конфигурации и ее работе, рекомендуется изучить документацию или код проекта. Вероятнее всего, эту архитектуру можно будет найти в месте, где используется балансировка нагрузки (load balancing) и/или шардинг (sharding).
Требуется увеличить в @Codebase количество шардов с 2 до 8.
Какие файлы для этого нужно поменять?
Какие изменения нужно внести в каждый из файлов?
Как при этом сократить количество кода до оптимального?
Увеличение числа шардов с 2 до 8 может быть реализовано путем изменения конфигурации балансировщика, а именно - в файле application.yml
указывается количество экземпляров сервиса Balance (они должны быть одинаковыми для всех шардов).
Также, на каждый новый шард необходимо подключаться к gRPC-серверу TransferServiceImpl. Для этого в файле TransferServiceImpl
нужно создать отдельные ManagedChannel для каждого дополнительного шарда и подключиться к сервису Balance через эти каналы.
В файле TransferServiceImpl
в методе transferMoney()
необходимо также учитывать новое количество шардов при распределении между ними, а именно - определить для каждой транзакции на каком шарде она будет выполняться.
При этом у вас должно быть 2 ManagedChannel (channel1, channel2) и количество шардов - это число зависит от количества проперти "balance.*.url" в файле конфигурации application.yml
.
Каждый измененный файл будет иметь следующие изменения:
-
В
application.yml
добавление url для каждого нового шарда (balance.2.url, balance.3.url и т.д.) -
Добавление в файл
TransferServiceImpl
методаgetChannel()
, который будет возвращать ManagedChannel по номеру шарда:private ManagedChannel getChannel(int instance) { return instance == 0 ? channel1 : channel2; }
-
Изменение в
TransferServiceImpl
методеtransferMoney()
распределение транзакций между шардами:int fromInstance = (int)request.getFromAccountId() % numOfShards; int toInstance = (int)request.getToAccountId() % numOfShards;
-
В
TransferServiceImpl
добавление нового ManagedChannel в конструкторе:for(int i = 2; i < numOfShards; ++i) { ManagedChannel channel = ManagedChannelBuilder.forTarget(env.getProperty("balance." + i + ".url")).usePlaintext().build(); channels.add(channel); }
Количество кода необходимо будет сократить, если было указано ранее - в файле TransferServiceImpl
добавив метод для получения ManagedChannel по номеру шарда и использовании его при необходимости.
Придумай ещё интересных сценариев для ручного тестирования кроме тех, что описаны в @README.md.
Для ручного тестирования gRPC-приложения можно использовать следующие сценарии:
-
Тестирование обработки ошибок: Проверить, как приложение реагирует на некорректные входные данные или исключения, которые могут возникнуть во время работы. Например, попытаться перевести больше денег, чем есть на балансе аккаунта.
-
Тестирование параллелизма: Проверить, как приложение обрабатывает запросы от нескольких пользователей одновременно. Например, попытаться сделать два перевода между двумя аккаунтами одновременно.
-
Тестирование времени ответа: Проверить время ответа приложения на запросы разного размера данных. Например, перевод маленькой суммы между двумя аккаунтами и большой суммы между ними.
-
Тестирование стабильности: Проверить, насколько приложение устойчиво к сбоям баз данных или другим ошибкам во время работы. Например, остановить PostgreSQL и проверить, как приложение реагирует на недоступность базы данных.
-
Тестирование нагрузочного тестирования: Проверить, насколько приложение может обработать большой объем запросов в определенном временном интервале. Например, сделать 100 одновременных переводов между двумя аккаунтами.
-
Тестирование отказоустойчивости: Проверить, как приложение работает, когда часть сервисов недоступна. Например, остановить один из экземпляров gRPC-приложений и проверить, будет ли приложение функционировать нормально.
--- prompts-gpt/1_balance.go 2024-01-20 23:57:53.142133568 +0300
+++ prompts/1_balance.go 2024-01-19 11:56:41.231864906 +0300
@@ -1,4 +1,4 @@
-package migrations
+package prompts
import (
"database/sql"
@@ -7,8 +7,8 @@
)
func init() {
- goose.AddMigration(upChangeRequest, downChangeRequest)
- goose.AddMigration(upGetDataResponse, downGetDataResponse)
+ goose.AddNamedMigration("1_change.go", upChangeRequest, downChangeRequest)
+ goose.AddNamedMigration("2_get.go", upGetDataResponse, downGetDataResponse)
}
func upChangeRequest(tx *sql.Tx) error {
@@ -31,7 +31,7 @@
_, err := tx.Exec(`CREATE TABLE get_data_responses (
balance bigint NOT NULL,
transaction_id bigint NOT NULL,
- amount bigint NOT NULL);`)
+ account_id bigint PRIMARY KEY);`)
return err
}
diff -Nurw prompts-gpt/build.gradle prompts/build.gradle
--- prompts-gpt/build.gradle 2024-01-20 05:46:20.088045871 +0300
+++ prompts/build.gradle 2024-01-20 05:50:15.176053045 +0300
@@ -13,8 +13,10 @@
}
dependencies {
- implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'net.devh:grpc-spring-boot-starter:2.15.0.RELEASE'
implementation 'io.grpc:grpc-netty-shaded:1.53.0'
implementation 'io.grpc:grpc-protobuf:1.53.0'
implementation 'io.grpc:grpc-stub:1.53.0'
+ implementation 'io.grpc:grpc-services:1.53.0'
+ implementation 'io.github.lognet:grpc-spring-boot-starter:5.1.5'
}
diff -Nurw prompts-gpt/cmd/testing/main.go prompts/cmd/testing/main.go
--- prompts-gpt/cmd/testing/main.go 2024-01-20 05:53:53.844059718 +0300
+++ prompts/cmd/testing/main.go 2024-01-21 00:05:56.546522426 +0300
@@ -2,16 +2,17 @@
import (
"context"
- "fmt"
+ "testing"
+ "time"
"google.golang.org/grpc"
)
-func TestTransferService(client pb.TransferClient, ctx context.Context, t *testing.T) {
- cases := []struct{fromAccountId, toAccountId int32; amount float32}{
+func TestTransferService(client TransferClient, ctx context.Context, t *testing.T) {
+ cases := []struct{fromAccountId, toAccountId, amount int64}{
{1, 2, 500},
}
for _, c := range cases {
- resp, err := client.TransferMoney(ctx, &pb.TransferRequest{FromAccountId: c.fromAccountId, ToAccountId: c.toAccountId, Amount: c.amount})
+ resp, err := client.TransferMoney(ctx, &TransferRequest{FromAccountId: c.fromAccountId, ToAccountId: c.toAccountId, Amount: c.amount})
if err != nil {
t.Fatalf("could not transfer money: %v", err)
}
@@ -20,11 +21,12 @@
}
// check if balance has changed correctly
- fromAccountData, err := client.GetAccountData(ctx, &pb.GetDataRequest{AccountId: c.fromAccountId})
+ fromAccountData, err := client.GetAccountData(ctx, &GetDataRequest{AccountId: c.fromAccountId})
if err != nil {
t.Fatalf("could not get from account data: %v", err)
}
if fromAccountData.GetBalance()+c.amount != 0 { // assuming initial balance is 1000 for account with id 1
+ println(fromAccountData.GetBalance(), c.amount)
t.Errorf("expected from account balance to be %f; got %f", 1000-c.amount, fromAccountData.GetBalance())
}
@@ -32,12 +34,13 @@
if len(fromAccountData.GetTransactions()) != 1 {
t.Errorf("expected 1 transaction; got %d", len(fromAccountData.GetTransactions()))
}
- if fromAccountData.GetTransactions()[0].GetAmount() != c.amount {
+ if fromAccountData.GetTransactions()[0].GetAmount()+c.amount != 0{
+ println(fromAccountData.GetTransactions()[0].GetAmount(), c.amount)
t.Errorf("expected transaction amount to be %f; got %f", c.amount, fromAccountData.GetTransactions()[0].GetAmount())
}
// check if balance has changed correctly for the recipient account
- toAccountData, err := client.GetAccountData(ctx, &pb.GetDataRequest{AccountId: c.toAccountId})
+ toAccountData, err := client.GetAccountData(ctx, &GetDataRequest{AccountId: c.toAccountId})
if err != nil {
t.Fatalf("could not get to account data: %v", err)
}
@@ -48,12 +51,13 @@
}
func main() {
+ t := &testing.T{}
conn, err := grpc.Dial("localhost:5002", grpc.WithInsecure()) // replace with your port
if err != nil {
- log.Fatalf("could not connect to server: %v", err)
+ t.Fatalf("could not connect to server: %v", err)
}
defer conn.Close()
- client := pb.NewTransferClient(conn)
+ client := NewTransferClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
TestTransferService(client, ctx, t) // replace t with your testing instance
diff -Nurw prompts-gpt/docker-compose.yaml prompts/docker-compose.yaml
--- prompts-gpt/docker-compose.yaml 2024-01-20 23:57:26.129899300 +0300
+++ prompts/docker-compose.yaml 2024-01-19 10:34:45.272499076 +0300
@@ -19,7 +19,7 @@
app1:
build:
context: .
- dockerfile: Dockerfile
+ dockerfile: go.Dockerfile
ports:
- "5000:5000"
environment:
@@ -30,7 +30,7 @@
app2:
build:
context: .
- dockerfile: Dockerfile
+ dockerfile: go.Dockerfile
ports:
- "5001:5001"
environment:
@@ -41,14 +41,16 @@
appnew:
build:
context: .
- dockerfile: Dockerfile
+ dockerfile: java.Dockerfile
ports:
- "5002:5002"
+ - "6565:6565"
environment:
- DB_URL: postgres://root:secret@db1:5432/testdb?sslmode=disable
- SERVER_ADDR: 0.0.0.0:5002
- balance.0.url: "app1:5000"
- balance.1.url: "app2:5001"
+ SERVER_PORT: 5002
+ balance_0_url: "app1:5000"
+ balance_1_url: "app2:5001"
depends_on:
- app1
- app2
+
+
diff -Nurw prompts-gpt/go.Dockerfile prompts/go.Dockerfile
--- prompts-gpt/go.Dockerfile 2024-01-20 06:17:27.040102845 +0300
+++ prompts/go.Dockerfile 2024-01-20 06:17:37.488103164 +0300
@@ -11,7 +11,7 @@
RUN go get -d -v ./...
# Compile the Go app
-RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/shard/main.go
# Use a separate image to keep intermediate container clean
FROM alpine:latest
diff -Nurw prompts-gpt/java.Dockerfile prompts/java.Dockerfile
--- prompts-gpt/java.Dockerfile 2024-01-20 05:46:35.444046339 +0300
+++ prompts/java.Dockerfile 2024-01-18 00:01:42.992056854 +0300
@@ -4,7 +4,7 @@
COPY . .
RUN gradle clean bootJar -x test
-FROM openjdk:17-jre
+FROM openjdk:17-slim
WORKDIR /app
COPY --from=build_env /app/build/libs/*.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]
diff -Nurw prompts-gpt/Makefile prompts/Makefile
--- prompts-gpt/Makefile 2024-01-20 05:48:13.632049336 +0300
+++ prompts/Makefile 2024-01-19 19:52:28.674016686 +0300
@@ -23,3 +23,4 @@
$(GENERATED_TRANSFER_FILES):
docker run -v $(PWD):/defs --rm $(DOCKER_IMAGE) -l go -d /defs -i /defs -o $(TRANSFER_GO_OUT) --go-source-relative $(TRANSFER_PROTO_FILE)
+ rm -f $(TRANSFER_GO_OUT)/balance*
\ В конце файла нет новой строки
diff -Nurw prompts-gpt/README.md prompts/README.md
--- prompts-gpt/README.md 2024-01-20 05:55:12.908062131 +0300
+++ prompts/README.md 2024-01-19 14:50:54.638908985 +0300
@@ -41,23 +41,22 @@
@startuml
actor User as u
+participant "TransferServiceImpl" as ts
participant "app1" as a1
participant "app2" as a2
-participant "TransferServiceImpl" as ts
participant "db1" as d1
participant "db2" as d2
-u -[hidden]-> ts: transferMoney(request)
-ts -[hidden]-> a1: changeBalance(changeFrom)
-a1 --[hidden]-> d1: INSERT INTO change_requests...
-d1 --[hidden]-> a1: ...RETURNING balance
-a1 -[hidden]-> ts: changeBalance(changeTo)
-ts -[hidden]-> a2: changeBalance(changeTo)
-a2 --[hidden]-> d2: INSERT INTO change_requests...
-d2 --[hidden]-> a2: ...RETURNING balance
-a1 -[hidden]-> ts: success
-a2 -[hidden]-> ts: success
-ts -[hidden]-> u: TransferResponse(success)
+u --> ts: transferMoney(request)
+ts --> a1: changeBalance(changeFrom)
+a1 ---> d1: INSERT INTO change_requests...
+d1 ---> a1: ...RETURNING balance
+a1 --> ts: success
+ts --> a2: changeBalance(changeTo)
+a2 ---> d2: INSERT INTO change_requests...
+d2 ---> a2: ...RETURNING balance
+a2 --> ts: success
+ts --> u: TransferResponse(success)
@enduml
`````` 1. `TransferServiceImpl` в Java принимает запрос на перевод денег с одного аккаунта на другой (`appnew`). ``````
@@ -77,7 +76,6 @@
`````` Вначале вам нужно создать файл `docker-compose.yaml`, который уже был предоставлен в вопросе. Переместитесь в директорию проекта и запустите следующие команды: ``````
`````` ```bash ``````
-cd /path/to/your/project
docker-compose up -d
`````` ``` ``````
@@ -90,7 +88,7 @@
Ваша задача - изменить баланс аккаунта с ID=1 на сумму +500:
`````` ```bash ``````
-grpcurl -d '{"accountId":1, "amount":500}' -plaintext localhost:5000 balance.Balance/ChangeBalance
+grpcurl -d '{"accountId":1, "amount":500, "transactionId": 1}' -plaintext localhost:5001 balance.Balance/ChangeBalance
`````` ``` ``````
После выполнения этой команды баланс аккаунта с ID=1 должен увеличиться на 500, и ваша gRPC-ответ должен быть следующим:
@@ -104,13 +102,13 @@
Теперь вы можете получить данные о балансе аккаунта с ID=1:
`````` ```bash ``````
-grpcurl -d '{"accountId":1}' -plaintext localhost:5000 balance.Balance/GetAccountData
+grpcurl -d '{"accountId":1}' -plaintext localhost:5001 balance.Balance/GetAccountData
`````` ``` ``````
Ваша gRPC-ответ должен быть следующим:
`````` ```json ``````
-{ "balance": 600, "transactions": [{"id":1,"amount":500}] }
+{ "balance": 500, "transactions": [{"id":1,"amount":500}] }
`````` ``` ``````
### 4. Тестирование метода TransferMoney
@@ -134,7 +132,7 @@
Ваша gRPC-ответ должен быть следующим:
`````` ```json ``````
-{ "balance": 100, "transactions": [{"id":1,"amount":500}] }
+{ "balance": 0, "transactions": [{"id":1,"amount":500}, {"id":2,"amount":-500}] }
`````` ``` ``````
и для аккаунта с ID=2:
@@ -146,5 +144,4 @@
Ваша gRPC-ответ должен быть следующим:
`````` ```json ``````
-{ "balance": 600, "transactions": [{"id":1,"amount":500}] }
``````-``` ``````
\ В конце файла нет новой строки
+{ "balance": 500, "transactions": [{"id":2,"amount":500}] }
diff -Nurw prompts-gpt/server.go prompts/server.go
--- prompts-gpt/server.go 2024-01-20 21:24:57.806410644 +0300
+++ prompts/server.go 2024-01-20 21:45:24.964018200 +0300
@@ -1,17 +1,18 @@
-package main
+package prompts
import (
"context"
"database/sql"
)
-type server struct {
- db *sql.DB
+type Server struct {
+ DB *sql.DB
+ UnimplementedBalanceServer
}
-func (s *server) ChangeBalance(ctx context.Context, in *ChangeRequest) (*ChangeResponse, error) {
+func (s *Server) ChangeBalance(ctx context.Context, in *ChangeRequest) (*ChangeResponse, error) {
// Begin a new transaction
- tx, err := s.db.BeginTx(ctx, nil)
+ tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return &ChangeResponse{Success: false}, err
}
@@ -26,7 +27,7 @@
}
// Perform UPSERT operation to update the balance in the database
- _, err = tx.Exec("INSERT INTO get_data_responses (balance, transaction_id, account_id) VALUES (balance + $1, $2, $3) ON CONFLICT (account_id) DO UPDATE SET balance = get_data_responses.balance + EXCLUDED.balance", in.Amount, in.TransactionId, in.AccountId)
+ _, err = tx.Exec("INSERT INTO get_data_responses (balance, transaction_id, account_id) VALUES ($1, $2, $3) ON CONFLICT (account_id) DO UPDATE SET balance = get_data_responses.balance + EXCLUDED.balance, transaction_id = EXCLUDED.transaction_id", in.Amount, in.TransactionId, in.AccountId)
if err != nil {
return &ChangeResponse{Success: false}, err
}
@@ -37,25 +38,25 @@
return &ChangeResponse{Success: false}, err
}
- return &ChangeResponse{Success: true, TransactionId: in.TransactionId}, nil
+ return &ChangeResponse{Success: true}, nil
}
-func (s *server) GetAccountData(ctx context.Context, in *GetDataRequest) (*GetDataResponse, error) {
+func (s *Server) GetAccountData(ctx context.Context, in *GetDataRequest) (*GetDataResponse, error) {
// Query the balance from the database
var balance int64
- err := s.db.QueryRow("SELECT balance FROM get_data_responses WHERE account_id = ?", in.AccountId).Scan(&balance)
+ err := s.DB.QueryRow("SELECT balance FROM get_data_responses WHERE account_id = $1", in.AccountId).Scan(&balance)
if err != nil {
return nil, err
}
// Query all transactions for the account from the database
- rows, err := s.db.Query("SELECT transaction_id, amount FROM change_requests WHERE account_id = ? ORDER BY transaction_id", in.AccountId)
+ rows, err := s.DB.Query("SELECT transaction_id, amount FROM change_requests WHERE account_id = $1 ORDER BY transaction_id", in.AccountId)
if err != nil {
return nil, err
}
defer rows.Close()
- var transactions []*Transaction
+ var transactions []*GetDataResponse_Transaction
for rows.Next() {
- t := &Transaction{}
+ t := &GetDataResponse_Transaction{}
err = rows.Scan(&t.Id, &t.Amount)
if err != nil {
return nil, err
diff -Nurw prompts-gpt/src/main/java/prompts/Application.java prompts/src/main/java/prompts/Application.java
--- prompts-gpt/src/main/java/prompts/Application.java 2024-01-20 05:46:07.368045482 +0300
+++ prompts/src/main/java/prompts/Application.java 2024-01-19 10:45:37.002714874 +0300
@@ -1,3 +1,5 @@
+package prompts;
+
import io.grpc.Server;
import io.grpc.protobuf.services.ProtoReflectionService;
import org.springframework.boot.SpringApplication;
diff -Nurw prompts-gpt/src/main/java/prompts/TransferServiceImpl.java prompts/src/main/java/prompts/TransferServiceImpl.java
--- prompts-gpt/src/main/java/prompts/TransferServiceImpl.java 2024-01-20 05:45:50.196044958 +0300
+++ prompts/src/main/java/prompts/TransferServiceImpl.java 2024-01-19 10:19:35.549384358 +0300
@@ -1,5 +1,9 @@
-import com.aitsvet.balance.*;
-import com.aitsvet.transfer.*;
+package prompts;
+
+import balance.*;
+import balance.BalanceOuterClass.*;
+import transfer.*;
+import transfer.TransferOuterClass.*;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;
@@ -28,11 +32,11 @@
int fromInstance = (int)request.getFromAccountId() % 2;
int toInstance = (int)request.getToAccountId() % 2;
- BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient1 =
- BalanceServiceGrpc.newBlockingStub(fromInstance == 0 ? channel1 : channel2);
+ BalanceGrpc.BalanceBlockingStub balanceClient1 =
+ BalanceGrpc.newBlockingStub(fromInstance == 0 ? channel1 : channel2);
- BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient2 =
- BalanceServiceGrpc.newBlockingStub(toInstance == 0 ? channel1 : channel2);
+ BalanceGrpc.BalanceBlockingStub balanceClient2 =
+ BalanceGrpc.newBlockingStub(toInstance == 0 ? channel1 : channel2);
ChangeRequest changeFrom = ChangeRequest.newBuilder()
.setTransactionId(System.currentTimeMillis())
@@ -54,18 +58,27 @@
}
@Override
- public void getAccountData(GetDataRequest request, StreamObserver<GetDataResponse> responseObserver) {
+ public void getAccountData(TransferOuterClass.GetDataRequest request, StreamObserver<TransferOuterClass.GetDataResponse> responseObserver) {
int instance = (int)request.getAccountId() % 2;
- BalanceServiceGrpc.BalanceServiceBlockingStub balanceClient =
- BalanceServiceGrpc.newBlockingStub(instance == 0 ? channel1 : channel2);
+ BalanceGrpc.BalanceBlockingStub balanceClient =
+ BalanceGrpc.newBlockingStub(instance == 0 ? channel1 : channel2);
- GetDataRequest dataRequest = GetDataRequest.newBuilder()
+ BalanceOuterClass.GetDataRequest dataRequest = BalanceOuterClass.GetDataRequest.newBuilder()
.setAccountId(request.getAccountId())
.build();
- GetDataResponse balanceResponse = balanceClient.getAccountData(dataRequest);
+ BalanceOuterClass.GetDataResponse balanceResponse = balanceClient.getAccountData(dataRequest);
+
+ var b = TransferOuterClass.GetDataResponse.newBuilder();
+ b.setBalance(balanceResponse.getBalance());
+ for (var t : balanceResponse.getTransactionsList()) {
+ var tb = TransferOuterClass.GetDataResponse.Transaction.newBuilder();
+ tb.setId(t.getId());
+ tb.setAmount(t.getAmount());
+ b.addTransactions(tb.build());
+ }
- responseObserver.onNext(balanceResponse);
+ responseObserver.onNext(b.build());
responseObserver.onCompleted();
}
}