├── frontend ├── src │ ├── ts │ │ ├── types.ts │ │ ├── enums.ts │ │ ├── interfaces.ts │ │ └── consts.ts │ ├── vite-env.d.ts │ ├── pages │ │ ├── Agents │ │ │ ├── AgentsPage.module.css │ │ │ └── AgentsPage.tsx │ │ ├── Operations │ │ │ ├── OperationPage.module.css │ │ │ └── OperationsPage.tsx │ │ ├── Expressions │ │ │ ├── ExpressionsPage.module.css │ │ │ └── ExpressionsPage.tsx │ │ └── Login │ │ │ ├── LoginPage.module.css │ │ │ └── LoginPage.tsx │ ├── App.css │ ├── components │ │ ├── Header │ │ │ ├── Header.module.css │ │ │ └── Header.tsx │ │ ├── Input │ │ │ ├── Input.module.css │ │ │ └── Input.tsx │ │ ├── OperationBlock │ │ │ ├── OperationBlock.module.css │ │ │ └── OperationBlock.tsx │ │ ├── Button │ │ │ ├── Button.tsx │ │ │ └── Button.module.css │ │ ├── LoginForm │ │ │ ├── LoginForm.module.css │ │ │ └── LoginForm.tsx │ │ ├── AgentBlock │ │ │ ├── AgentBlock.module.css │ │ │ └── AgentBlock.tsx │ │ └── ExpressionBlock │ │ │ ├── ExpressionBlock.module.css │ │ │ └── ExpressionBlock.tsx │ ├── main.tsx │ ├── App.tsx │ └── services │ │ └── api.ts ├── public │ ├── calculator.png │ └── vite.svg ├── vite.config.ts ├── tsconfig.node.json ├── index.html ├── .eslintrc.cjs ├── tsconfig.json └── package.json ├── images ├── ERD.png ├── agents.png ├── schema.png └── expressions.png ├── sql ├── schema │ ├── 0002_agents_created_at.sql │ ├── 0006_agents_add_number_of_active_calc.sql │ ├── 0003_users.sql │ ├── 0001_agents.sql │ ├── 0005_operations.sql │ └── 0004_expressions.sql ├── sqlc.yaml ├── queries │ ├── users.sql │ ├── operations.sql │ ├── agents.sql │ └── expressions.sql └── daec.sql ├── docker ├── database │ └── postgres.Dockerfile ├── frontend │ └── frontend.Dockerfile └── backend │ ├── auth.Dockerfile │ ├── agent.Dockerfile │ └── orchestrator.Dockerfile ├── .dockerignore ├── backend ├── internal │ ├── lib │ │ ├── logger │ │ │ ├── sl │ │ │ │ └── sl.go │ │ │ ├── setup │ │ │ │ ├── pretty_logger.go │ │ │ │ └── logger.go │ │ │ └── handlers │ │ │ │ ├── slogdiscard │ │ │ │ └── slogdiscard.go │ │ │ │ └── slogpretty │ │ │ │ └── slogpretty.go │ │ ├── pool │ │ │ └── pool.go │ │ └── jwt │ │ │ └── jwt.go │ ├── domain │ │ ├── brokers │ │ │ ├── consumer.go │ │ │ └── producer.go │ │ └── messages │ │ │ └── expression_message.go │ ├── storage │ │ ├── postgres │ │ │ ├── db.go │ │ │ ├── users.sql.go │ │ │ ├── operations.sql.go │ │ │ ├── model_transformers.go │ │ │ ├── models.go │ │ │ ├── agents.sql.go │ │ │ └── expressions.sql.go │ │ └── storage.go │ ├── protos │ │ ├── proto │ │ │ └── daec │ │ │ │ └── daec.proto │ │ └── gen │ │ │ └── go │ │ │ └── daec │ │ │ ├── daec_grpc.pb.go │ │ │ └── daec.pb.go │ ├── rabbitmq │ │ ├── rabbitmq.go │ │ ├── amqp_consumer.go │ │ └── amqp_producer.go │ ├── agent │ │ ├── simple_computer.go │ │ └── agent.go │ ├── http-server │ │ ├── handlers │ │ │ ├── agent.go │ │ │ ├── jsonhandler.go │ │ │ ├── operation.go │ │ │ ├── user.go │ │ │ └── expression.go │ │ └── middleware │ │ │ └── logger │ │ │ └── logger.go │ ├── orchestrator │ │ ├── orchestrator_task.go │ │ ├── parser │ │ │ ├── infix_to_postfix.go │ │ │ ├── tokens.go │ │ │ ├── infix_to_postfix_test.go │ │ │ ├── parser.go │ │ │ ├── tokens_test.go │ │ │ └── parser_test.go │ │ └── orchestrator.go │ ├── app │ │ ├── grpc │ │ │ └── app.go │ │ ├── agent │ │ │ └── app.go │ │ └── orchestrator │ │ │ └── app.go │ ├── config │ │ └── config.go │ ├── grpc │ │ └── auth │ │ │ └── server.go │ └── services │ │ └── auth │ │ └── auth.go ├── config │ └── local.yaml ├── go.mod ├── cmd │ ├── auth │ │ └── main.go │ ├── agent │ │ └── main.go │ └── orchestrator │ │ └── main.go └── go.sum ├── .github └── workflows │ ├── golangci-lint.yml │ └── golangci-test.yml ├── .gitignore ├── docker-compose.yml └── README.md /frontend/src/ts/types.ts: -------------------------------------------------------------------------------- 1 | export type FormVariant = "login" | "reg"; -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /images/ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prrromanssss/DAEC-fullstack/HEAD/images/ERD.png -------------------------------------------------------------------------------- /images/agents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prrromanssss/DAEC-fullstack/HEAD/images/agents.png -------------------------------------------------------------------------------- /images/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prrromanssss/DAEC-fullstack/HEAD/images/schema.png -------------------------------------------------------------------------------- /images/expressions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prrromanssss/DAEC-fullstack/HEAD/images/expressions.png -------------------------------------------------------------------------------- /frontend/public/calculator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prrromanssss/DAEC-fullstack/HEAD/frontend/public/calculator.png -------------------------------------------------------------------------------- /frontend/src/pages/Agents/AgentsPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 20px; 5 | } -------------------------------------------------------------------------------- /frontend/src/pages/Operations/OperationPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 20px; 5 | } -------------------------------------------------------------------------------- /sql/schema/0002_agents_created_at.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | ALTER TABLE agents ADD COLUMN created_at timestamp NOT NULL; 3 | 4 | -- +goose Down 5 | ALTER TABLE agents DROP COLUMN created_at; -------------------------------------------------------------------------------- /sql/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - schema: "schema" 4 | queries: "queries" 5 | engine: "postgresql" 6 | gen: 7 | go: 8 | out: "internal/storage/postgres" -------------------------------------------------------------------------------- /docker/database/postgres.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:16-alpine3.19 2 | 3 | ENV POSTGRES_PASSWORD=postgres 4 | 5 | ENV POSTGRES_DB=daec 6 | 7 | COPY ./sql/daec.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | min-width: 320px; 4 | min-height: 100vh; 5 | } 6 | 7 | #root { 8 | margin: 0 auto; 9 | } 10 | 11 | .page { 12 | padding: 20px; 13 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Frontend modules 2 | frontend/node_modules/ 3 | 4 | # Backend modules 5 | backend/vendor 6 | 7 | # Database 8 | sql/sqlc.yaml 9 | sql/queries 10 | sql/schema 11 | 12 | # Github 13 | images/ 14 | .github/ -------------------------------------------------------------------------------- /backend/internal/lib/logger/sl/sl.go: -------------------------------------------------------------------------------- 1 | package sl 2 | 3 | import "log/slog" 4 | 5 | func Err(err error) slog.Attr { 6 | return slog.Attr{ 7 | Key: "error", 8 | Value: slog.StringValue(err.Error()), 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background: blueviolet; 3 | padding: 20px; 4 | color: white; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | gap: 50px; 9 | } -------------------------------------------------------------------------------- /frontend/src/components/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | padding: 15px; 3 | border-radius: 10px; 4 | border: 1px solid blueviolet; 5 | min-width: 300px; 6 | } 7 | 8 | .input:focus-visible { 9 | outline: none; 10 | } -------------------------------------------------------------------------------- /sql/schema/0006_agents_add_number_of_active_calc.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | ALTER TABLE agents ADD COLUMN number_of_active_calculations int NOT NULL DEFAULT 0; 3 | 4 | -- +goose Down 5 | ALTER TABLE agents DROP COLUMN number_of_active_calculations; -------------------------------------------------------------------------------- /frontend/src/components/OperationBlock/OperationBlock.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | margin: 0; 3 | margin-bottom: 5px; 4 | font-size: 20px; 5 | } 6 | 7 | .block { 8 | display: flex; 9 | align-items: center; 10 | gap: 20px; 11 | } -------------------------------------------------------------------------------- /sql/queries/users.sql: -------------------------------------------------------------------------------- 1 | -- name: GetUser :one 2 | SELECT user_id, email, password_hash 3 | FROM users 4 | WHERE email = $1; 5 | 6 | -- name: SaveUser :one 7 | INSERT INTO users 8 | (email, password_hash) 9 | VALUES 10 | ($1, $2) 11 | RETURNING user_id; -------------------------------------------------------------------------------- /frontend/src/pages/Expressions/ExpressionsPage.module.css: -------------------------------------------------------------------------------- 1 | .actionsBlock { 2 | display: flex; 3 | align-items: center; 4 | gap: 20px; 5 | } 6 | 7 | .items { 8 | margin-top: 20px; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 10px; 12 | } -------------------------------------------------------------------------------- /backend/internal/domain/brokers/consumer.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import "github.com/streadway/amqp" 4 | 5 | // Consumer is an interface to consume messages from queue. 6 | type Consumer interface { 7 | GetMessages() <-chan amqp.Delivery 8 | Close() 9 | } 10 | -------------------------------------------------------------------------------- /docker/frontend/frontend.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY frontend/package.json . 6 | 7 | RUN npm install 8 | 9 | COPY frontend/ . 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 5173 14 | 15 | CMD [ "npm", "run", "dev", "--", "--host"] -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | src: "/src" 10 | } 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /sql/schema/0003_users.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE IF NOT EXISTS users ( 3 | user_id int GENERATED ALWAYS AS IDENTITY, 4 | email text UNIQUE NOT NULL, 5 | password_hash bytea NOT NULL, 6 | 7 | PRIMARY KEY(user_id) 8 | ); 9 | 10 | -- +goose Down 11 | DROP TABLE IF EXISTS users; -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client' 2 | import App from './App.tsx' 3 | import { ToastContainer } from 'react-toastify'; 4 | import 'react-toastify/dist/ReactToastify.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | <> 8 | 9 | 10 | , 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Button.module.css"; 2 | import { ButtonProps } from "src/ts/interfaces"; 3 | 4 | export const Button = ({ title, onClick, disabled }: ButtonProps) => { 5 | return ( 6 | 13 | ) 14 | } -------------------------------------------------------------------------------- /frontend/src/components/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | background: blueviolet; 3 | color: white; 4 | padding: 15px 20px; 5 | border-radius: 10px; 6 | border: none; 7 | outline: none; 8 | cursor: pointer; 9 | } 10 | 11 | .btn:hover { 12 | box-shadow: 2px 1px 5px gray; 13 | } 14 | 15 | .btn:disabled { 16 | background: #d1d1d1; 17 | box-shadow: none; 18 | cursor: not-allowed; 19 | } -------------------------------------------------------------------------------- /backend/internal/domain/brokers/producer.go: -------------------------------------------------------------------------------- 1 | package brokers 2 | 3 | import ( 4 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 5 | "github.com/Prrromanssss/DAEC-fullstack/internal/rabbitmq" 6 | ) 7 | 8 | // Producer is an interface to produce messages to queue. 9 | type Producer interface { 10 | PublishExpressionMessage(msg *messages.ExpressionMessage) error 11 | Reconnect() (*rabbitmq.AMQPProducer, error) 12 | Close() 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Input.module.css"; 2 | import { InputProps } from "src/ts/interfaces"; 3 | 4 | export const Input = ({ value, onChange, type, placeholder }: InputProps) => { 5 | return ( 6 | onChange(e.target.value)} 12 | /> 13 | ) 14 | } -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Distributed arithmetic expression calculator 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/LoginForm/LoginForm.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | border-radius: 20px; 3 | padding: 20px; 4 | display: flex; 5 | align-items: center; 6 | flex-direction: column; 7 | border: 1px solid black; 8 | width: 100%; 9 | max-width: 300px; 10 | gap: 10px; 11 | 12 | } 13 | 14 | .input { 15 | padding: 10px; 16 | border-radius: 10px; 17 | border: 1px solid black; 18 | width: 100%; 19 | } 20 | 21 | .button { 22 | padding: 10px; 23 | } -------------------------------------------------------------------------------- /backend/internal/lib/logger/setup/pretty_logger.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/handlers/slogpretty" 8 | ) 9 | 10 | func SetupPrettySlog() *slog.Logger { 11 | opts := slogpretty.PrettyHandlerOptions{ 12 | SlogOpts: &slog.HandlerOptions{ 13 | Level: slog.LevelDebug, 14 | }, 15 | } 16 | 17 | handler := opts.NewPrettyHandler(os.Stdout) 18 | return slog.New(handler) 19 | } 20 | -------------------------------------------------------------------------------- /docker/backend/auth.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | WORKDIR /build 4 | 5 | ADD backend/go.mod . 6 | 7 | COPY backend . 8 | 9 | RUN go build -o auth cmd/auth/main.go 10 | 11 | FROM alpine 12 | 13 | WORKDIR /build 14 | 15 | COPY --from=builder /build/auth /build/auth 16 | COPY backend/config/local.yaml /app/backend/config/local.yaml 17 | 18 | ENV JWT_SECRET "super-super-secret" 19 | 20 | ENV CONFIG_PATH /app/backend/config/local.yaml 21 | 22 | CMD ["./auth"] -------------------------------------------------------------------------------- /docker/backend/agent.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | WORKDIR /build 4 | 5 | ADD backend/go.mod . 6 | 7 | COPY backend . 8 | 9 | RUN go build -o agent cmd/agent/main.go 10 | 11 | FROM alpine 12 | 13 | WORKDIR /build 14 | 15 | COPY --from=builder /build/agent /build/agent 16 | COPY backend/config/local.yaml /app/backend/config/local.yaml 17 | 18 | ENV JWT_SECRET "super-super-secret" 19 | 20 | ENV CONFIG_PATH /app/backend/config/local.yaml 21 | 22 | CMD ["./agent"] -------------------------------------------------------------------------------- /frontend/src/components/AgentBlock/AgentBlock.module.css: -------------------------------------------------------------------------------- 1 | .agentBlock { 2 | border: 1px solid blueviolet; 3 | width: fit-content; 4 | padding: 10px; 5 | border-radius: 10px; 6 | } 7 | 8 | .title { 9 | margin: 0; 10 | font-size: 20px; 11 | } 12 | 13 | .icon { 14 | width: 30px; 15 | height: 30px; 16 | } 17 | 18 | .headerBlock { 19 | display: flex; 20 | align-items: center; 21 | gap: 10px; 22 | } 23 | 24 | .text { 25 | margin: 0; 26 | margin-top: 10px; 27 | font-size: 20px; 28 | } -------------------------------------------------------------------------------- /frontend/src/pages/Login/LoginPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | flex-direction: column; 8 | } 9 | 10 | .containerBtn { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | gap: 5px; 15 | margin-bottom: 10px; 16 | } 17 | 18 | .activeElement { 19 | cursor: pointer; 20 | padding: 5px 10px; 21 | border-radius: 10px; 22 | border: 1px solid black; 23 | } -------------------------------------------------------------------------------- /frontend/src/ts/enums.ts: -------------------------------------------------------------------------------- 1 | export enum ROUTES { 2 | EXPRESSIONS = "Expressions", 3 | OPERATIONS = "Operations", 4 | AGENTS = "Agents", 5 | LOGIN = "Login", 6 | } 7 | 8 | export enum EXPRESSION_STATUS { 9 | READY_FOR_COMPUTATION = "ready_for_computation", 10 | COMPUTING = "computing", 11 | RESULT = "result", 12 | TERMINATED = "terminated", 13 | } 14 | 15 | export enum AGENT_STATUS { 16 | RUNNING = "running", 17 | WAITING = "waiting", 18 | SLEEPING = "sleeping", 19 | TERMINATED = "terminated", 20 | } -------------------------------------------------------------------------------- /docker/backend/orchestrator.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | 3 | WORKDIR /build 4 | 5 | ADD backend/go.mod . 6 | 7 | COPY backend . 8 | 9 | RUN go build -o orchestrator cmd/orchestrator/main.go 10 | 11 | FROM alpine 12 | 13 | WORKDIR /build 14 | 15 | COPY --from=builder /build/orchestrator /build/orchestrator 16 | COPY backend/config/local.yaml /app/backend/config/local.yaml 17 | 18 | ENV JWT_SECRET "super-super-secret" 19 | 20 | ENV CONFIG_PATH /app/backend/config/local.yaml 21 | 22 | CMD ["./orchestrator"] -------------------------------------------------------------------------------- /frontend/src/components/ExpressionBlock/ExpressionBlock.module.css: -------------------------------------------------------------------------------- 1 | .expressionBlock { 2 | border: 1px solid blueviolet; 3 | width: fit-content; 4 | padding: 10px; 5 | border-radius: 10px; 6 | } 7 | 8 | .title { 9 | margin: 0; 10 | font-size: 20px; 11 | } 12 | 13 | .icon { 14 | width: 30px; 15 | height: 30px; 16 | } 17 | 18 | .headerBlock { 19 | display: flex; 20 | align-items: center; 21 | gap: 10px; 22 | } 23 | 24 | .createdAt { 25 | margin: 0; 26 | margin-top: 10px; 27 | font-size: 20px; 28 | } -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /sql/schema/0001_agents.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | DROP TYPE IF EXISTS agent_status; 3 | CREATE TYPE agent_status AS ENUM ('running', 'waiting', 'sleeping', 'terminated'); 4 | 5 | CREATE TABLE IF NOT EXISTS agents ( 6 | agent_id int GENERATED ALWAYS AS IDENTITY, 7 | number_of_parallel_calculations int NOT NULL DEFAULT 5, 8 | last_ping timestamp NOT NULL, 9 | status agent_status NOT NULL, 10 | 11 | PRIMARY KEY(agent_id) 12 | ); 13 | 14 | -- +goose Down 15 | DROP TABLE IF EXISTS agents; 16 | DROP TYPE IF EXISTS agent_status; -------------------------------------------------------------------------------- /sql/schema/0005_operations.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE IF NOT EXISTS operations ( 3 | operation_id int GENERATED ALWAYS AS IDENTITY, 4 | operation_type varchar(1) NOT NULL, 5 | execution_time int NOT NULL DEFAULT 100, 6 | user_id int NOT NULL, 7 | 8 | PRIMARY KEY(operation_id), 9 | CONSTRAINT operation_type_user_id UNIQUE(operation_type, user_id), 10 | FOREIGN KEY(user_id) 11 | REFERENCES users(user_id) 12 | ON DELETE CASCADE 13 | ); 14 | 15 | -- +goose Down 16 | DROP TABLE IF EXISTS operations; -------------------------------------------------------------------------------- /backend/internal/domain/messages/expression_message.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | type ExpressionMessage struct { 4 | ExpressionID int32 `json:"expression_id"` 5 | Token string `json:"token"` 6 | Expression string `json:"expression"` 7 | Result int `json:"result"` 8 | IsPing bool `json:"is_ping"` 9 | AgentID int32 `json:"agent_id"` 10 | UserID int32 `json:"user_id"` 11 | Kill bool `json:"kill"` 12 | } 13 | 14 | type ResultAndTokenMessage struct { 15 | Result string `json:"result"` 16 | Token string `json:"token"` 17 | } 18 | -------------------------------------------------------------------------------- /backend/config/local.yaml: -------------------------------------------------------------------------------- 1 | env: "local" 2 | inactive_time_for_agent: 20 3 | time_for_ping: 10 4 | tokenTTL: 1h 5 | grpc_server: 6 | address: ":44044" 7 | grpc_client_connection_string: "auth:44044" 8 | rabbit_queue: 9 | rabbitmq_url: "amqp://guest:guest@rabbitmq:5672/" 10 | queue_for_expressions_to_agents: "Expressions to agents" 11 | queue_for_results_from_agents: "Results from agents" 12 | http_server: 13 | address: ":3000" 14 | timeout: 4s 15 | idle_timeout: 60s 16 | database_instance: 17 | goose_migration_dir: "./backend/sql/schema" 18 | storage_url: "postgres://postgres:postgres@db:5432/daec?sslmode=disable" -------------------------------------------------------------------------------- /frontend/src/pages/Agents/AgentsPage.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./AgentsPage.module.css"; 2 | import { useEffect, useState } from "react"; 3 | import { Agent } from "src/ts/interfaces"; 4 | import { getAgents } from "src/services/api"; 5 | import { AgentBlock } from "src/components/AgentBlock/AgentBlock"; 6 | 7 | export const AgentsPage = () => { 8 | const [agents, setAgents] = useState([]); 9 | 10 | useEffect(() => { 11 | getAgents() 12 | .then(data => setAgents(data)); 13 | }, []); 14 | 15 | return ( 16 |
17 | {agents.map(agent => )} 18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /backend/internal/lib/logger/setup/logger.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | ) 7 | 8 | const ( 9 | envLocal = "local" 10 | envDev = "dev" 11 | envProd = "prod" 12 | ) 13 | 14 | func SetupLogger(env string) *slog.Logger { 15 | var log *slog.Logger 16 | 17 | switch env { 18 | case envLocal: 19 | log = SetupPrettySlog() 20 | case envDev: 21 | log = slog.New( 22 | slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 23 | Level: slog.LevelDebug, 24 | }), 25 | ) 26 | case envProd: 27 | log = slog.New( 28 | slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 29 | Level: slog.LevelInfo, 30 | }), 31 | ) 32 | } 33 | return log 34 | } 35 | -------------------------------------------------------------------------------- /sql/queries/operations.sql: -------------------------------------------------------------------------------- 1 | -- name: UpdateOperationTime :one 2 | UPDATE operations 3 | SET execution_time = $1 4 | WHERE operation_type = $2 AND user_id = $3 5 | RETURNING operation_id, operation_type, execution_time, user_id; 6 | 7 | -- name: GetOperations :many 8 | SELECT 9 | operation_id, operation_type, execution_time, user_id 10 | FROM operations 11 | WHERE user_id = $1 12 | ORDER BY operation_type DESC; 13 | 14 | -- name: GetOperationTimeByType :one 15 | SELECT execution_time 16 | FROM operations 17 | WHERE operation_type = $1 AND user_id = $2; 18 | 19 | -- name: NewOperationsForUser :exec 20 | INSERT INTO operations (operation_type, user_id) VALUES 21 | ('+', $1), 22 | ('-', $1), 23 | ('*', $1), 24 | ('/', $1); -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Header.module.css"; 2 | import { ROUTES } from "src/ts/enums"; 3 | import { HeaderProps } from "src/ts/interfaces"; 4 | 5 | export const Header = ({ activePage, setActivePage }: HeaderProps) => { 6 | return ( 7 |
8 | {Object.values(ROUTES).map(page => { 9 | const isActive = activePage === page; 10 | return ( 11 |

setActivePage(page)} 14 | style={{ 15 | textDecoration: isActive ? "underline" : "none" 16 | }} 17 | > 18 | {page} 19 |

20 | ) 21 | })} 22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /backend/internal/storage/postgres/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | 5 | package postgres 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | branches: 9 | - master 10 | - main 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | golangci: 17 | strategy: 18 | matrix: 19 | go: ['1.21'] 20 | os: [macos-latest] 21 | name: lint 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go }} 28 | cache: false 29 | - name: golangci-lint 30 | uses: golangci/golangci-lint-action@v4 31 | with: 32 | version: v1.54 33 | working-directory: backend 34 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "src/*": ["src/*"] 25 | } 26 | }, 27 | "include": ["src"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /backend/internal/protos/proto/daec/daec.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package auth; 4 | 5 | option go_package = "prrromanssss.daec.v1;daecv1"; 6 | 7 | service Auth { 8 | rpc Register (RegisterRequest) returns (RegisterResponse); 9 | rpc Login(LoginRequest) returns (LoginResponse); 10 | } 11 | 12 | message RegisterRequest { 13 | string email = 1; // Email of the user to register. 14 | string password = 2; // Password of ther user to register. 15 | } 16 | 17 | message RegisterResponse { 18 | int64 user_id = 1; // User ID of the registered user. 19 | } 20 | 21 | message LoginRequest { 22 | string email = 1; // Email of the user to login. 23 | string password = 2; // Password of ther user to login. 24 | } 25 | 26 | message LoginResponse { 27 | string token = 1; // ID token of the logged user. 28 | } -------------------------------------------------------------------------------- /backend/internal/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 7 | "github.com/streadway/amqp" 8 | ) 9 | 10 | type AMQPConfig struct { 11 | log *slog.Logger 12 | conn *amqp.Connection 13 | } 14 | 15 | // NewAMQPConfig creates new AMQP connection. 16 | func NewAMQPConfig(log *slog.Logger, amqpUrl string) (*AMQPConfig, error) { 17 | conn, err := amqp.Dial(amqpUrl) 18 | if err != nil { 19 | log.Error("can't connect to RabbitMQ", sl.Err(err)) 20 | return nil, err 21 | } 22 | 23 | log.Info("successfully connected to RabbitMQ instance") 24 | 25 | return &AMQPConfig{ 26 | log: log, 27 | conn: conn, 28 | }, nil 29 | } 30 | 31 | // Close closes AMQP connection. 32 | func (ac *AMQPConfig) Close() { 33 | ac.conn.Close() 34 | } 35 | -------------------------------------------------------------------------------- /backend/internal/agent/simple_computer.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 7 | ) 8 | 9 | // simpleComputer calculates a simple expression consisting of 2 operands. 10 | func simpleComputer( 11 | exprMsg *messages.ExpressionMessage, 12 | digit1, digit2 int, 13 | oper string, 14 | timer *time.Timer, 15 | res chan<- *messages.ExpressionMessage, 16 | ) { 17 | switch { 18 | case oper == "+": 19 | <-timer.C 20 | exprMsg.Result = digit1 + digit2 21 | res <- exprMsg 22 | case oper == "-": 23 | <-timer.C 24 | exprMsg.Result = digit1 - digit2 25 | res <- exprMsg 26 | case oper == "/": 27 | <-timer.C 28 | exprMsg.Result = digit1 / digit2 29 | res <- exprMsg 30 | case oper == "*": 31 | <-timer.C 32 | exprMsg.Result = digit1 * digit2 33 | res <- exprMsg 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/internal/http-server/handlers/agent.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 9 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 10 | ) 11 | 12 | // HandlerGetAgents is a http.Handler to get all agents from storage. 13 | func HandlerGetAgents(log *slog.Logger, dbCfg *storage.Storage) http.HandlerFunc { 14 | return func(w http.ResponseWriter, r *http.Request) { 15 | const fn = "handlers.HandlerGetAgents" 16 | 17 | log := log.With( 18 | slog.String("fn", fn), 19 | ) 20 | 21 | agents, err := dbCfg.Queries.GetAgents(r.Context()) 22 | if err != nil { 23 | respondWithError(log, w, 400, fmt.Sprintf("couldn't get agents: %v", err)) 24 | return 25 | } 26 | 27 | respondWithJson(log, w, 200, postgres.DatabaseAgentsToAgents(agents)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/golangci-test.yml: -------------------------------------------------------------------------------- 1 | name: golangci-test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | branches: 9 | - master 10 | - main 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | golangci: 17 | strategy: 18 | matrix: 19 | go: ['1.21'] 20 | os: [macos-latest] 21 | name: test 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go }} 28 | cache: false 29 | - name: Run tests 30 | run: | 31 | if grep -q "ok" <<< "$(cd backend && go test ./...)"; then 32 | echo "All tests passed" 33 | exit 0 34 | else 35 | echo "Some tests failed" 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /frontend/src/components/ExpressionBlock/ExpressionBlock.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./ExpressionBlock.module.css"; 2 | import { EXPRESSION_DESCRIPTION, ICONS } from "src/ts/consts"; 3 | import { ExpressionBlockProps } from "src/ts/interfaces"; 4 | 5 | export const ExpressionBlock = ({ expression }: ExpressionBlockProps) => { 6 | const date = new Date(expression.created_at).toLocaleString(); 7 | 8 | return ( 9 |
10 |
11 | 15 |

16 | {expression.data} = {expression.is_ready ? expression.result : "?"} ({EXPRESSION_DESCRIPTION[expression.status]}) 17 |

18 |
19 |

20 | Created at: {date} 21 |

22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roma-calc", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.6.7", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-toastify": "^10.0.4" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.55", 20 | "@types/react-dom": "^18.2.19", 21 | "@typescript-eslint/eslint-plugin": "^6.21.0", 22 | "@typescript-eslint/parser": "^6.21.0", 23 | "@vitejs/plugin-react": "^4.2.1", 24 | "eslint": "^8.56.0", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "eslint-plugin-react-refresh": "^0.4.5", 27 | "typescript": "^5.2.2", 28 | "vite": "^5.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sql/schema/0004_expressions.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | DROP TYPE IF EXISTS expression_status; 3 | CREATE TYPE expression_status AS ENUM ('ready_for_computation', 'computing', 'result', 'terminated'); 4 | 5 | CREATE TABLE IF NOT EXISTS expressions ( 6 | expression_id int GENERATED ALWAYS AS IDENTITY, 7 | user_id int NOT NULL, 8 | agent_id int, 9 | created_at timestamp NOT NULL, 10 | updated_at timestamp NOT NULL, 11 | data text NOT NULL, 12 | parse_data text NOT NULL, 13 | status expression_status NOT NULL, 14 | result int NOT NULL DEFAULT 0, 15 | is_ready boolean NOT NULL DEFAULT false, 16 | 17 | PRIMARY KEY(expression_id), 18 | FOREIGN KEY(agent_id) 19 | REFERENCES agents(agent_id) 20 | ON DELETE SET NULL, 21 | FOREIGN KEY(user_id) 22 | REFERENCES users(user_id) 23 | ON DELETE CASCADE 24 | ); 25 | 26 | -- +goose Down 27 | DROP TABLE IF EXISTS expressions; 28 | DROP TYPE IF EXISTS expression_status; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | .env 23 | local.env 24 | 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | pnpm-debug.log* 32 | lerna-debug.log* 33 | 34 | node_modules 35 | dist 36 | dist-ssr 37 | *.local 38 | 39 | # Editor directories and files 40 | .vscode/* 41 | !.vscode/extensions.json 42 | .idea 43 | .DS_Store 44 | *.suo 45 | *.ntvs* 46 | *.njsproj 47 | *.sln 48 | *.sw? 49 | -------------------------------------------------------------------------------- /backend/internal/lib/logger/handlers/slogdiscard/slogdiscard.go: -------------------------------------------------------------------------------- 1 | package slogdiscard 2 | 3 | import ( 4 | "context" 5 | 6 | "log/slog" 7 | ) 8 | 9 | func NewDiscardLogger() *slog.Logger { 10 | return slog.New(NewDiscardHandler()) 11 | } 12 | 13 | type DiscardHandler struct{} 14 | 15 | func NewDiscardHandler() *DiscardHandler { 16 | return &DiscardHandler{} 17 | } 18 | 19 | func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { 20 | // Просто игнорируем запись журнала 21 | return nil 22 | } 23 | 24 | func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { 25 | // Возвращает тот же обработчик, так как нет атрибутов для сохранения 26 | return h 27 | } 28 | 29 | func (h *DiscardHandler) WithGroup(_ string) slog.Handler { 30 | // Возвращает тот же обработчик, так как нет группы для сохранения 31 | return h 32 | } 33 | 34 | func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { 35 | // Всегда возвращает false, так как запись журнала игнорируется 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/LoginForm/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./LoginForm.module.css"; 2 | import { FormProps } from "src/ts/interfaces"; 3 | 4 | export const LoginForm = ({ variant, handler, data, setData }: FormProps) => { 5 | return ( 6 |
7 | setData({ ...data, email: e.target.value })} 10 | value={data.email} 11 | className={styles.input} 12 | placeholder="Email..." 13 | /> 14 | setData({ ...data, password: e.target.value })} 18 | value={data.password} 19 | className={styles.input} 20 | placeholder="Password..." 21 | /> 22 | 29 |
30 | ) 31 | } -------------------------------------------------------------------------------- /frontend/src/components/OperationBlock/OperationBlock.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./OperationBlock.module.css"; 2 | import { OperationBlockProps } from "src/ts/interfaces" 3 | import { useState } from "react"; 4 | import { Button } from "../Button/Button"; 5 | import { Input } from "../Input/Input"; 6 | 7 | export const OperationBlock = ({ operation, saveChanges }: OperationBlockProps) => { 8 | const [operationName, setOperationName] = useState(Number(operation.execution_time)); 9 | const isChanged = operationName !== operation.execution_time; 10 | 11 | return ( 12 |
13 |

Operation type (sec): {operation.operation_type}

14 |
15 | setOperationName(Number(e))} 19 | /> 20 |
26 |
27 | ) 28 | } -------------------------------------------------------------------------------- /backend/internal/storage/postgres/users.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | // source: users.sql 5 | 6 | package postgres 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const getUser = `-- name: GetUser :one 13 | SELECT user_id, email, password_hash 14 | FROM users 15 | WHERE email = $1 16 | ` 17 | 18 | func (q *Queries) GetUser(ctx context.Context, email string) (User, error) { 19 | row := q.db.QueryRowContext(ctx, getUser, email) 20 | var i User 21 | err := row.Scan(&i.UserID, &i.Email, &i.PasswordHash) 22 | return i, err 23 | } 24 | 25 | const saveUser = `-- name: SaveUser :one 26 | INSERT INTO users 27 | (email, password_hash) 28 | VALUES 29 | ($1, $2) 30 | RETURNING user_id 31 | ` 32 | 33 | type SaveUserParams struct { 34 | Email string 35 | PasswordHash []byte 36 | } 37 | 38 | func (q *Queries) SaveUser(ctx context.Context, arg SaveUserParams) (int32, error) { 39 | row := q.db.QueryRowContext(ctx, saveUser, arg.Email, arg.PasswordHash) 40 | var user_id int32 41 | err := row.Scan(&user_id) 42 | return user_id, err 43 | } 44 | -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Prrromanssss/DAEC-fullstack 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/go-chi/chi v1.5.5 7 | github.com/go-chi/cors v1.2.1 8 | github.com/joho/godotenv v1.5.1 // indirect 9 | ) 10 | 11 | require github.com/lib/pq v1.10.9 12 | 13 | require ( 14 | github.com/fatih/color v1.16.0 15 | github.com/go-chi/chi/v5 v5.0.12 16 | github.com/golang-jwt/jwt/v5 v5.2.1 17 | github.com/ilyakaznacheev/cleanenv v1.5.0 18 | github.com/streadway/amqp v1.1.0 19 | golang.org/x/crypto v0.22.0 20 | google.golang.org/grpc v1.63.2 21 | google.golang.org/protobuf v1.33.0 22 | ) 23 | 24 | require ( 25 | github.com/BurntSushi/toml v1.2.1 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | golang.org/x/net v0.21.0 // indirect 29 | golang.org/x/sys v0.19.0 // indirect 30 | golang.org/x/text v0.14.0 // indirect 31 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /frontend/src/components/AgentBlock/AgentBlock.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./AgentBlock.module.css"; 2 | import { AGENT_DESCRIPTION, ICONS } from "src/ts/consts"; 3 | import { AgentBlockProps } from "src/ts/interfaces"; 4 | 5 | export const AgentBlock = ({ agent }: AgentBlockProps) => { 6 | const createdAt = new Date(agent.created_at).toLocaleString(); 7 | const computingAt = new Date(agent.last_ping).toLocaleString(); 8 | 9 | return ( 10 |
11 |
12 | 16 |

17 | Computing server ({AGENT_DESCRIPTION[agent.status]}) 18 |

19 |
20 |

21 | Last ping: {computingAt} 22 |

23 |

24 | Number of parallel calculations: {agent.number_of_parallel_calculations} 25 |

26 |

27 | Number of active calculations: {agent.number_of_active_calculations} 28 |

29 |

30 | Created at: {createdAt} 31 |

32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /backend/internal/http-server/middleware/logger/logger.go: -------------------------------------------------------------------------------- 1 | package mwlogger 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "log/slog" 8 | 9 | "github.com/go-chi/chi/v5/middleware" 10 | ) 11 | 12 | // New creates http.Handler with logging of all actions. 13 | func New(log *slog.Logger) func(next http.Handler) http.Handler { 14 | return func(next http.Handler) http.Handler { 15 | log := log.With( 16 | slog.String("component", "middleware/logger"), 17 | ) 18 | 19 | log.Info("logger middleware enabled") 20 | 21 | fn := func(w http.ResponseWriter, r *http.Request) { 22 | entry := log.With( 23 | slog.String("method", r.Method), 24 | slog.String("path", r.URL.Path), 25 | slog.String("remote_addr", r.RemoteAddr), 26 | slog.String("user_agent", r.UserAgent()), 27 | slog.String("request_id", middleware.GetReqID(r.Context())), 28 | ) 29 | ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) 30 | 31 | t1 := time.Now() 32 | defer func() { 33 | entry.Info("request completed", 34 | slog.Int("status", ww.Status()), 35 | slog.Int("bytes", ww.BytesWritten()), 36 | slog.String("duration", time.Since(t1).String()), 37 | ) 38 | }() 39 | 40 | next.ServeHTTP(ww, r) 41 | } 42 | 43 | return http.HandlerFunc(fn) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/orchestrator_task.go: -------------------------------------------------------------------------------- 1 | package orchestrator 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/brokers" 7 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/pool" 9 | "github.com/streadway/amqp" 10 | ) 11 | 12 | type orchestratorTask struct { 13 | orchestrator *Orchestrator 14 | ctx context.Context 15 | msgFromAgents amqp.Delivery 16 | producer brokers.Producer 17 | } 18 | 19 | // ExecuteWrapper is a wrapper function to call the Execute method with the necessary arguments. 20 | func ExecuteWrapper(o *Orchestrator, ctx context.Context, msgFromAgents amqp.Delivery, producer brokers.Producer) pool.PoolTask { 21 | return &orchestratorTask{o, ctx, msgFromAgents, producer} 22 | } 23 | 24 | // Execute implements the Execute method of the PoolTask interface 25 | func (ot *orchestratorTask) Execute() error { 26 | return ot.orchestrator.HandleMessagesFromAgents(ot.ctx, ot.msgFromAgents, ot.producer) 27 | } 28 | 29 | // OnFailure implements the OnFailure method of the PoolTask interface 30 | func (ot *orchestratorTask) OnFailure(err error) { 31 | ot.orchestrator.log.Error("orchestrator error", sl.Err(err)) 32 | ot.orchestrator.kill() 33 | } 34 | -------------------------------------------------------------------------------- /backend/cmd/auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | grpcapp "github.com/Prrromanssss/DAEC-fullstack/internal/app/grpc" 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/config" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/setup" 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/services/auth" 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 14 | ) 15 | 16 | func main() { 17 | // Load Config 18 | cfg := config.MustLoad() 19 | 20 | // Configuration Logger 21 | log := setup.SetupLogger(cfg.Env) 22 | log.Info( 23 | "start grpc server", 24 | slog.String("env", cfg.Env), 25 | slog.String("version", "2"), 26 | ) 27 | log.Debug("debug messages are enabled") 28 | 29 | // Configuration Storage 30 | dbCfg := storage.NewStorage(log, cfg.StorageURL) 31 | 32 | authService := auth.New(log, dbCfg, dbCfg, cfg.TokenTTL) 33 | 34 | grpcApp := grpcapp.New(log, authService, cfg.GRPCServer.Address) 35 | 36 | go grpcApp.MustRun() 37 | 38 | // Graceful shotdown 39 | stop := make(chan os.Signal, 1) 40 | signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) 41 | 42 | sign := <-stop 43 | 44 | log.Info("stopping application", slog.String("signal", sign.String())) 45 | 46 | grpcApp.Stop() 47 | 48 | log.Info("grpc server stopped") 49 | } 50 | -------------------------------------------------------------------------------- /backend/cmd/agent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | agentapp "github.com/Prrromanssss/DAEC-fullstack/internal/app/agent" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/config" 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/setup" 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 14 | ) 15 | 16 | func main() { 17 | ctxWithCancel, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | // Load Config 21 | cfg := config.MustLoad() 22 | 23 | // Configuration Logger 24 | log := setup.SetupLogger(cfg.Env) 25 | log.Info( 26 | "start agent", 27 | slog.String("env", cfg.Env), 28 | slog.String("version", "2"), 29 | ) 30 | 31 | // Configuration Storage 32 | dbCfg := storage.NewStorage(log, cfg.StorageURL) 33 | 34 | // Configuration Agent 35 | application, err := agentapp.New(log, cfg, dbCfg, cancel) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | go application.MustRun(ctxWithCancel) 41 | 42 | // Graceful shotdown 43 | stop := make(chan os.Signal, 1) 44 | signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) 45 | 46 | sign := <-stop 47 | 48 | log.Info("stopping agent", slog.String("signal", sign.String())) 49 | 50 | application.Stop(ctxWithCancel) 51 | 52 | log.Info("agent stopped") 53 | } 54 | -------------------------------------------------------------------------------- /backend/internal/http-server/handlers/jsonhandler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "log/slog" 6 | "net/http" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 9 | ) 10 | 11 | // The respondWithError function is designed to handle HTTP responses that indicate an error condition. 12 | func respondWithError(log *slog.Logger, w http.ResponseWriter, code int, msg string) { 13 | if code > 499 { 14 | log.Warn("responding with 5XX error", slog.Int("code", code)) 15 | } 16 | type errResponse struct { 17 | Error string `json:"error"` 18 | } 19 | 20 | respondWithJson(log, w, code, errResponse{ 21 | Error: msg, 22 | }) 23 | } 24 | 25 | // The respondWithJson function is a utility function designed to send HTTP responses with JSON content 26 | func respondWithJson(log *slog.Logger, w http.ResponseWriter, code int, payload interface{}) { 27 | data, err := json.Marshal(payload) 28 | if err != nil { 29 | log.Error("failed to marshal JSON response", sl.Err(err), slog.Any("payload", payload)) 30 | w.WriteHeader(500) 31 | 32 | return 33 | } 34 | 35 | w.Header().Add("Content-Type", "application/json") 36 | 37 | w.WriteHeader(code) 38 | _, err = w.Write(data) 39 | if err != nil { 40 | log.Error("failed to write data", sl.Err(err), slog.Any("payload", payload)) 41 | w.WriteHeader(500) 42 | 43 | return 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/pages/Operations/OperationsPage.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./OperationPage.module.css"; 2 | import { useEffect, useState } from "react"; 3 | import { Operation } from "src/ts/interfaces"; 4 | import { OperationBlock } from "src/components/OperationBlock/OperationBlock"; 5 | import { getOperations, updateOperation } from "src/services/api"; 6 | import { toast } from 'react-toastify'; 7 | 8 | export const OperationsPage = () => { 9 | const [operations, setOperations] = useState([]); 10 | 11 | const saveChanges = (newValue: number, operation: Operation) => { 12 | updateOperation({ ...operation, execution_time: newValue }) 13 | .then(() => { 14 | toast.success("Success"); 15 | getOperations() 16 | .then(data => setOperations(data)); 17 | }) 18 | .catch((err) => { 19 | toast.error(err.response.data.error); 20 | }); 21 | }; 22 | 23 | useEffect(() => { 24 | getOperations() 25 | .then(data => setOperations(data)) 26 | .catch(err => { 27 | toast.error(err.response.data.error); 28 | }); 29 | }, []); 30 | 31 | return ( 32 |
33 | {operations.map(operation => ( 34 | saveChanges(newValue, operation)} 38 | /> 39 | ))} 40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import { useEffect, useState } from 'react'; 3 | import { Header } from './components/Header/Header' 4 | import { ROUTES } from './ts/enums'; 5 | import { AgentsPage } from './pages/Agents/AgentsPage'; 6 | import { ExpressionsPage } from './pages/Expressions/ExpressionsPage'; 7 | import { OperationsPage } from './pages/Operations/OperationsPage'; 8 | import { LoginPage } from './pages/Login/LoginPage'; 9 | import axios from 'axios'; 10 | 11 | function App() { 12 | const [activePage, setActivePage] = useState(""); 13 | 14 | const changePage = (page: ROUTES) => { 15 | sessionStorage.setItem("page", page); 16 | setActivePage(page); 17 | } 18 | 19 | useEffect(() => { 20 | const pageFromStorage = sessionStorage.getItem("page") || ROUTES.EXPRESSIONS; 21 | const token = sessionStorage.getItem("token"); 22 | if (pageFromStorage) setActivePage(pageFromStorage); 23 | if (token) axios.defaults.headers.common = { "Authorization": `Bearer ${token}` }; 24 | }, []); 25 | 26 | return ( 27 | <> 28 |
32 |
33 | {activePage === ROUTES.AGENTS && } 34 | {activePage === ROUTES.EXPRESSIONS && } 35 | {activePage === ROUTES.OPERATIONS && } 36 | {activePage === ROUTES.LOGIN && } 37 |
38 | 39 | ) 40 | } 41 | 42 | export default App 43 | -------------------------------------------------------------------------------- /backend/internal/lib/pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | type WorkerPool interface { 9 | Start() 10 | Stop() 11 | AddWork(PoolTask) 12 | } 13 | 14 | type PoolTask interface { 15 | Execute() error 16 | OnFailure(error) 17 | } 18 | 19 | type MyPool struct { 20 | tasks chan PoolTask 21 | wg sync.WaitGroup 22 | isExecuting bool 23 | onceStart sync.Once 24 | onceStop sync.Once 25 | numWorkers int 26 | } 27 | 28 | func NewWorkerPool(numWorkers int, channelSize int) (*MyPool, error) { 29 | if numWorkers <= 0 { 30 | return nil, fmt.Errorf("incorect numWorkers") 31 | } 32 | if channelSize < 0 { 33 | return nil, fmt.Errorf("negative channelSize") 34 | } 35 | return &MyPool{ 36 | tasks: make(chan PoolTask, channelSize), 37 | isExecuting: false, 38 | numWorkers: numWorkers, 39 | }, nil 40 | } 41 | 42 | func (mp *MyPool) Start() { 43 | mp.onceStart.Do(func() { 44 | mp.wg.Add(mp.numWorkers) 45 | for i := 0; i < mp.numWorkers; i++ { 46 | go func() { 47 | defer mp.wg.Done() 48 | for pt := range mp.tasks { 49 | err := pt.Execute() 50 | if err != nil { 51 | pt.OnFailure(err) 52 | } 53 | } 54 | }() 55 | } 56 | mp.isExecuting = true 57 | }) 58 | } 59 | 60 | func (mp *MyPool) Stop() { 61 | mp.onceStop.Do(func() { 62 | mp.isExecuting = false 63 | close(mp.tasks) 64 | mp.wg.Wait() 65 | }) 66 | 67 | } 68 | 69 | func (mp *MyPool) AddWork(pt PoolTask) { 70 | if mp.isExecuting { 71 | mp.tasks <- pt 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { Agent, Expression, FormData, Operation } from "src/ts/interfaces"; 3 | 4 | axios.defaults.baseURL = "http://localhost:3000/v1"; 5 | 6 | export const getExpressions = async (): Promise => { 7 | const { data } = await axios.get("/expressions"); 8 | return data; 9 | } 10 | 11 | export const createExpression = async (name: string): Promise => { 12 | const { data } = await axios.post("/expressions", { data: name }); 13 | return data; 14 | } 15 | 16 | export const getOperations = async (): Promise => { 17 | const { data } = await axios.get("/operations"); 18 | return data; 19 | } 20 | 21 | export const updateOperation = async (operation: Operation): Promise => { 22 | const { operation_type, execution_time } = operation; 23 | const { data } = await axios.patch("/operations", { operation_type, execution_time }); 24 | return data; 25 | } 26 | 27 | export const getAgents = async (): Promise => { 28 | const { data } = await axios.get("/agents"); 29 | return data; 30 | } 31 | 32 | export const login = async (value: FormData): Promise<{ token: string }> => { 33 | const { data } = await axios.post("/login", value); 34 | sessionStorage.setItem("token", data.token); 35 | axios.defaults.headers.common = { "Authorization": `Bearer ${data.token}` } 36 | return data; 37 | } 38 | 39 | export const registration = async (value: FormData): Promise<{ user_id: number }> => { 40 | const { data } = await axios.post("/register", value); 41 | return data; 42 | } -------------------------------------------------------------------------------- /backend/internal/app/grpc/app.go: -------------------------------------------------------------------------------- 1 | package grpcapp 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net" 7 | 8 | authgrpc "github.com/Prrromanssss/DAEC-fullstack/internal/grpc/auth" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | type App struct { 13 | log *slog.Logger 14 | gRPCServer *grpc.Server 15 | address string 16 | } 17 | 18 | // MustRun runs gRPC server and panics if any error occurs. 19 | func (a *App) MustRun() { 20 | if err := a.Run(); err != nil { 21 | panic(err) 22 | } 23 | } 24 | 25 | // New creates new gRPC server app. 26 | func New( 27 | log *slog.Logger, 28 | authService authgrpc.Auth, 29 | address string, 30 | ) *App { 31 | gRPCServer := grpc.NewServer() 32 | 33 | authgrpc.Register(gRPCServer, authService) 34 | 35 | return &App{ 36 | log: log, 37 | gRPCServer: gRPCServer, 38 | address: address, 39 | } 40 | } 41 | 42 | func (a *App) Run() error { 43 | const op = "grpcapp.Run" 44 | 45 | log := a.log.With( 46 | slog.String("op", op), 47 | slog.String("address", a.address), 48 | ) 49 | 50 | lis, err := net.Listen("tcp", a.address) 51 | if err != nil { 52 | return fmt.Errorf("%s: %w", op, err) 53 | } 54 | log.Info("gRPC server is running", slog.String("addr", lis.Addr().String())) 55 | 56 | if err := a.gRPCServer.Serve(lis); err != nil { 57 | return fmt.Errorf("%s: %w", op, err) 58 | } 59 | return nil 60 | } 61 | 62 | // Stop stops gRPC server. 63 | func (a *App) Stop() { 64 | const op = "grpcapp.Stop" 65 | 66 | a.log.With(slog.String("op", op)). 67 | Info("stopping gRPC server") 68 | 69 | a.gRPCServer.GracefulStop() 70 | } 71 | -------------------------------------------------------------------------------- /backend/internal/rabbitmq/amqp_consumer.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 7 | "github.com/streadway/amqp" 8 | ) 9 | 10 | type AMQPConsumer struct { 11 | log *slog.Logger 12 | amqpCfg *AMQPConfig 13 | Queue amqp.Queue 14 | Channel *amqp.Channel 15 | Messages <-chan amqp.Delivery 16 | } 17 | 18 | // NewAMQPConsumer creates new Consumer for AMQP protocol. 19 | func NewAMQPConsumer( 20 | log *slog.Logger, 21 | amqpCfg *AMQPConfig, 22 | queueName string, 23 | ) (*AMQPConsumer, error) { 24 | chCons, err := amqpCfg.conn.Channel() 25 | if err != nil { 26 | log.Error("can't create a channel from RabbitMQ", sl.Err(err)) 27 | return nil, err 28 | } 29 | queue, err := chCons.QueueDeclare( 30 | queueName, 31 | false, 32 | false, 33 | false, 34 | false, 35 | nil, 36 | ) 37 | if err != nil { 38 | log.Error("can't create a RabbitMQ queue", sl.Err(err)) 39 | return nil, err 40 | } 41 | msgs, err := chCons.Consume( 42 | queue.Name, 43 | "", 44 | false, 45 | false, 46 | false, 47 | false, 48 | nil, 49 | ) 50 | if err != nil { 51 | log.Error("can't create a channel to consume messages from RabbitMQ", sl.Err(err)) 52 | return nil, err 53 | } 54 | return &AMQPConsumer{ 55 | log: log, 56 | amqpCfg: amqpCfg, 57 | Queue: queue, 58 | Channel: chCons, 59 | Messages: msgs, 60 | }, nil 61 | } 62 | 63 | // GetMessages returns messages from the Consumer channel. 64 | func (ac *AMQPConsumer) GetMessages() <-chan amqp.Delivery { 65 | return ac.Messages 66 | } 67 | 68 | // Close closes Consumer channel. 69 | func (ac *AMQPConsumer) Close() { 70 | ac.Channel.Close() 71 | } 72 | -------------------------------------------------------------------------------- /sql/queries/agents.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateAgent :one 2 | INSERT INTO agents 3 | (created_at, number_of_parallel_calculations, last_ping, status) 4 | VALUES 5 | ($1, $2, $3, $4) 6 | RETURNING 7 | agent_id, number_of_parallel_calculations, 8 | last_ping, status, created_at, 9 | number_of_active_calculations; 10 | 11 | -- name: GetAgents :many 12 | SELECT 13 | agent_id, number_of_parallel_calculations, 14 | last_ping, status, created_at, 15 | number_of_active_calculations 16 | FROM agents 17 | ORDER BY created_at DESC; 18 | 19 | -- name: UpdateAgentLastPing :exec 20 | UPDATE agents 21 | SET last_ping = $1 22 | WHERE agent_id = $2; 23 | 24 | -- name: UpdateAgentStatus :exec 25 | UPDATE agents 26 | SET status = $1 27 | WHERE agent_id = $2; 28 | 29 | -- name: UpdateTerminateAgentByID :exec 30 | UPDATE agents 31 | SET status = 'terminated', number_of_active_calculations = 0 32 | WHERE agent_id = $1; 33 | 34 | -- name: DecrementNumberOfActiveCalculations :exec 35 | UPDATE agents 36 | SET number_of_active_calculations = number_of_active_calculations - 1 37 | WHERE agent_id = $1; 38 | 39 | -- name: IncrementNumberOfActiveCalculations :exec 40 | UPDATE agents 41 | SET number_of_active_calculations = number_of_active_calculations + 1 42 | WHERE agent_id = $1; 43 | 44 | -- name: TerminateAgents :many 45 | UPDATE agents 46 | SET status = 'terminated', number_of_active_calculations = 0 47 | WHERE EXTRACT(SECOND FROM NOW()::timestamp - agents.last_ping) > $1::numeric 48 | RETURNING agent_id; 49 | 50 | -- name: TerminateOldAgents :exec 51 | DELETE FROM agents 52 | WHERE status = 'terminated'; 53 | 54 | -- name: GetBusyAgents :many 55 | SELECT agent_id 56 | FROM agents 57 | WHERE number_of_active_calculations != 0; -------------------------------------------------------------------------------- /frontend/src/ts/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ROUTES, EXPRESSION_STATUS, AGENT_STATUS } from "./enums"; 2 | import { FormVariant } from "./types"; 3 | 4 | export interface Expression { 5 | expression_id: number, 6 | created_at: string, 7 | updated_at: string, 8 | data: string, 9 | status: EXPRESSION_STATUS, 10 | is_ready: boolean, 11 | result: number, 12 | parse_data: string, 13 | user_id: number, 14 | agent_id: number, 15 | } 16 | 17 | export interface Operation { 18 | operation_id: number, 19 | operation_type: string, 20 | execution_time: number, 21 | user_id: number, 22 | } 23 | 24 | export interface Agent { 25 | agent_id: number, 26 | number_of_parallel_calculations: number, 27 | last_ping: string, 28 | status: AGENT_STATUS, 29 | created_at: string, 30 | number_of_active_calculations: number, 31 | } 32 | 33 | export interface HeaderProps { 34 | activePage: ROUTES | string, 35 | setActivePage: (value: ROUTES) => void, 36 | } 37 | 38 | export interface OperationBlockProps { 39 | operation: Operation, 40 | saveChanges: (value: number) => void; 41 | } 42 | 43 | export interface ButtonProps { 44 | title: string, 45 | onClick: () => void, 46 | disabled?: boolean, 47 | } 48 | 49 | export interface InputProps { 50 | value: string | number, 51 | onChange: (value: string) => void, 52 | type?: string; 53 | placeholder?: string; 54 | } 55 | 56 | export interface ExpressionBlockProps { 57 | expression: Expression, 58 | } 59 | 60 | export interface AgentBlockProps { 61 | agent: Agent, 62 | } 63 | 64 | export interface FormData { 65 | email: string, 66 | password: string, 67 | } 68 | 69 | export interface FormProps { 70 | variant: FormVariant, 71 | handler: () => void; 72 | data: FormData; 73 | setData: (data: FormData) => void; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/parser/infix_to_postfix.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // InfixToPostfix translates an expression from infix record to postfix. 9 | func InfixToPostfix(expression string) (string, error) { 10 | var output strings.Builder 11 | var stack []rune 12 | for _, char := range expression { 13 | switch char { 14 | case '(': 15 | stack = append(stack, char) 16 | case ')': 17 | err := popUntilOpeningParenthesis(&stack, &output) 18 | if err != nil { 19 | return "", err 20 | } 21 | case '+', '-', '*', '/': 22 | popOperatorsWithHigherPrecedence(char, &stack, &output) 23 | stack = append(stack, char) 24 | output.WriteRune(' ') 25 | default: 26 | output.WriteRune(char) 27 | } 28 | } 29 | 30 | for len(stack) > 0 { 31 | popTopOperator(&stack, &output) 32 | } 33 | 34 | return strings.ReplaceAll(strings.TrimSpace(output.String()), " ", " "), nil 35 | } 36 | 37 | func popUntilOpeningParenthesis(stack *[]rune, output *strings.Builder) error { 38 | for len(*stack) > 0 && (*stack)[len(*stack)-1] != '(' { 39 | popTopOperator(stack, output) 40 | } 41 | if len(*stack) == 0 { 42 | return errors.New("invalid expression") 43 | } 44 | *stack = (*stack)[:len(*stack)-1] 45 | return nil 46 | } 47 | 48 | func popOperatorsWithHigherPrecedence(operator rune, stack *[]rune, output *strings.Builder) { 49 | for len(*stack) > 0 && precedence((*stack)[len(*stack)-1]) >= precedence(operator) { 50 | popTopOperator(stack, output) 51 | } 52 | } 53 | 54 | func popTopOperator(stack *[]rune, output *strings.Builder) { 55 | output.WriteRune(' ') 56 | output.WriteRune((*stack)[len(*stack)-1]) 57 | *stack = (*stack)[:len(*stack)-1] 58 | } 59 | 60 | func precedence(operator rune) int { 61 | switch operator { 62 | case '+', '-': 63 | return 1 64 | case '*', '/': 65 | return 2 66 | default: 67 | return 0 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/src/ts/consts.ts: -------------------------------------------------------------------------------- 1 | import { AGENT_STATUS, EXPRESSION_STATUS } from "./enums"; 2 | 3 | const greenIcon: string = "https://thumbs.dreamstime.com/b/%D0%B7%D0%B5%D0%BB%D0%B5%D0%BD%D0%B0%D1%8F-%D0%B3%D0%B0%D0%BB%D0%BE%D1%87%D0%BA%D0%B0-%D0%BF%D0%BE%D0%B4%D1%82%D0%B2%D0%B5%D1%80%D0%B6%D0%B4%D0%B0%D1%8E%D1%82-%D0%B8%D0%BB%D0%B8-%D0%BB%D0%B8%D0%BD%D0%B8%D1%8F-%D0%B7%D0%BD%D0%B0%D1%87%D0%BA%D0%B8-%D0%BA%D0%BE%D0%BD%D1%82%D1%80%D0%BE%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9-%D0%BF%D0%BE%D0%BC%D0%B5%D1%82%D0%BA%D0%B8-185588596.jpg"; 4 | const yellowIcon: string = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSeendsSC1KLb1ljM9hILUWPjJDcu_JPgdvsymh6DZj0YqBILgplXiIpWlELjhynu4R-9M&usqp=CAU"; 5 | const redIcon: string = "https://cdn-icons-png.flaticon.com/512/6368/6368418.png"; 6 | const blackIcon: string = "https://cdn-icons-png.flaticon.com/512/7351/7351882.png"; 7 | 8 | export const ICONS = { 9 | [AGENT_STATUS.RUNNING]: blackIcon, 10 | [AGENT_STATUS.SLEEPING]: greenIcon, 11 | [AGENT_STATUS.WAITING]: yellowIcon, 12 | [AGENT_STATUS.TERMINATED]: redIcon, 13 | [EXPRESSION_STATUS.READY_FOR_COMPUTATION]: yellowIcon, 14 | [EXPRESSION_STATUS.RESULT]: greenIcon, 15 | [EXPRESSION_STATUS.COMPUTING]: blackIcon, 16 | } as const; 17 | 18 | export const AGENT_DESCRIPTION = { 19 | [AGENT_STATUS.RUNNING]: "the server is calculating expressions and waiting for new ones", 20 | [AGENT_STATUS.SLEEPING]: "the server is calculating the expressions and is fully occupied", 21 | [AGENT_STATUS.WAITING]: "the server is waiting for new expressions", 22 | [AGENT_STATUS.TERMINATED]: "the server is down", 23 | } as const; 24 | 25 | export const EXPRESSION_DESCRIPTION = { 26 | [EXPRESSION_STATUS.READY_FOR_COMPUTATION]: "the expression is accepted, it will be processed soon", 27 | [EXPRESSION_STATUS.RESULT]: "the expression is ready", 28 | [EXPRESSION_STATUS.COMPUTING]: "the expression is being processed, it will be calculated soon", 29 | [EXPRESSION_STATUS.TERMINATED]: "agent was terminated", 30 | } as const; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | db: 4 | build: 5 | context: . 6 | dockerfile: ./docker/database/postgres.Dockerfile 7 | container_name: daec-db 8 | restart: always 9 | environment: 10 | POSTGRES_USER: "postgres" 11 | POSTGRES_PASSWORD: "postgres" 12 | POSTGRES_DB: "daec" 13 | ports: 14 | - "5432:5432" 15 | volumes: 16 | - daec-data:/var/lib/postgresql/data 17 | rabbitmq: 18 | image: rabbitmq:3-management 19 | container_name: daec-rabbitmq 20 | restart: unless-stopped 21 | ports: 22 | - "5672:5672" 23 | orchestrator: 24 | build: 25 | context: . 26 | dockerfile: ./docker/backend/orchestrator.Dockerfile 27 | container_name: daec-orchestrator 28 | restart: always 29 | ports: 30 | - "3000:3000" 31 | depends_on: 32 | - db 33 | - rabbitmq 34 | agent1: 35 | build: 36 | context: . 37 | dockerfile: ./docker/backend/agent.Dockerfile 38 | container_name: daec-agent-1 39 | restart: unless-stopped 40 | depends_on: 41 | - rabbitmq 42 | agent2: 43 | build: 44 | context: . 45 | dockerfile: ./docker/backend/agent.Dockerfile 46 | container_name: daec-agent-2 47 | restart: unless-stopped 48 | depends_on: 49 | - rabbitmq 50 | agent3: 51 | build: 52 | context: . 53 | dockerfile: ./docker/backend/agent.Dockerfile 54 | container_name: daec-agent-3 55 | restart: unless-stopped 56 | depends_on: 57 | - rabbitmq 58 | auth: 59 | build: 60 | context: . 61 | dockerfile: ./docker/backend/auth.Dockerfile 62 | container_name: daec-auth 63 | restart: always 64 | ports: 65 | - "44044:44044" 66 | frontend: 67 | build: 68 | context: . 69 | dockerfile: ./docker/frontend/frontend.Dockerfile 70 | container_name: daec-frontend 71 | restart: unless-stopped 72 | ports: 73 | - "5173:5173" 74 | 75 | volumes: 76 | daec-data: -------------------------------------------------------------------------------- /backend/internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "log/slog" 8 | "os" 9 | 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 12 | "github.com/lib/pq" 13 | ) 14 | 15 | var ( 16 | ErrUserExists = errors.New("user already exists") 17 | ErrUserNotFound = errors.New("user not found") 18 | ErrAppNotFound = errors.New("app not found") 19 | ) 20 | 21 | type Storage struct { 22 | Queries *postgres.Queries 23 | DB *sql.DB 24 | } 25 | 26 | // NewStorage creates new Storage. 27 | func NewStorage(log *slog.Logger, dbURL string) *Storage { 28 | const fn = "storage.NewStorage" 29 | 30 | log = log.With( 31 | slog.String("fn", fn), 32 | ) 33 | 34 | conn, err := sql.Open("postgres", dbURL) 35 | 36 | if err != nil { 37 | log.Error("can't connect to database:", sl.Err(err)) 38 | os.Exit(1) 39 | } 40 | 41 | db := postgres.New(conn) 42 | 43 | log.Info("successfully connected to DB instance") 44 | 45 | return &Storage{ 46 | Queries: db, 47 | DB: conn, 48 | } 49 | } 50 | 51 | // SaveUser saves user to storage. 52 | func (s *Storage) SaveUser(ctx context.Context, email string, passHash []byte) (int32, error) { 53 | userID, err := s.Queries.SaveUser(ctx, postgres.SaveUserParams{ 54 | Email: email, 55 | PasswordHash: passHash, 56 | }) 57 | if err != nil { 58 | if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" && pqErr.Constraint == "users_email_key" { 59 | // Handle duplicate key violation error 60 | return 0, ErrUserExists // Return the error unchanged 61 | } 62 | return 0, err 63 | } 64 | 65 | return userID, nil 66 | } 67 | 68 | // User gets user from storage. 69 | func (s *Storage) User(ctx context.Context, email string) (postgres.User, error) { 70 | user, err := s.Queries.GetUser(ctx, email) 71 | if err != nil { 72 | return postgres.User{}, err 73 | } 74 | 75 | return user, nil 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/pages/Expressions/ExpressionsPage.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./ExpressionsPage.module.css"; 2 | import { useEffect, useState } from "react"; 3 | import { Expression } from "src/ts/interfaces"; 4 | import { Button } from "src/components/Button/Button"; 5 | import { Input } from "src/components/Input/Input"; 6 | import { createExpression, getExpressions } from "src/services/api"; 7 | import { ExpressionBlock } from "src/components/ExpressionBlock/ExpressionBlock"; 8 | import { toast } from 'react-toastify'; 9 | 10 | export const ExpressionsPage = () => { 11 | const [expressions, setExpressions] = useState([]); 12 | const [newExpression, setNewExpression] = useState(""); 13 | 14 | const createHandler = () => { 15 | createExpression(newExpression) 16 | .then(() => { 17 | toast.success("Success"); 18 | getExpressions() 19 | .then(data => setExpressions(data)); 20 | }) 21 | .catch((err) => { 22 | toast.error(err.response.data.error); 23 | }) 24 | .finally(() => { 25 | setNewExpression(""); 26 | }); 27 | }; 28 | 29 | useEffect(() => { 30 | getExpressions() 31 | .then(data => setExpressions(data)) 32 | .catch(err => { 33 | toast.error(err.response.data.error); 34 | }); 35 | }, []); 36 | 37 | return ( 38 |
39 |
40 | setNewExpression(e)} 44 | /> 45 |
51 |
52 | {expressions.map(expression => ( 53 | 57 | ))} 58 |
59 |
60 | ) 61 | } -------------------------------------------------------------------------------- /sql/daec.sql: -------------------------------------------------------------------------------- 1 | DROP TYPE IF EXISTS agent_status; 2 | CREATE TYPE agent_status AS ENUM ('running', 'waiting', 'sleeping', 'terminated'); 3 | 4 | CREATE TABLE IF NOT EXISTS agents ( 5 | agent_id int GENERATED ALWAYS AS IDENTITY, 6 | number_of_parallel_calculations int NOT NULL DEFAULT 5, 7 | last_ping timestamp NOT NULL, 8 | status agent_status NOT NULL, 9 | 10 | PRIMARY KEY(agent_id) 11 | ); 12 | 13 | ALTER TABLE agents ADD COLUMN created_at timestamp NOT NULL; 14 | 15 | CREATE TABLE IF NOT EXISTS users ( 16 | user_id int GENERATED ALWAYS AS IDENTITY, 17 | email text UNIQUE NOT NULL, 18 | password_hash bytea NOT NULL, 19 | 20 | PRIMARY KEY(user_id) 21 | ); 22 | 23 | DROP TYPE IF EXISTS expression_status; 24 | CREATE TYPE expression_status AS ENUM ('ready_for_computation', 'computing', 'result', 'terminated'); 25 | 26 | CREATE TABLE IF NOT EXISTS expressions ( 27 | expression_id int GENERATED ALWAYS AS IDENTITY, 28 | user_id int NOT NULL, 29 | agent_id int, 30 | created_at timestamp NOT NULL, 31 | updated_at timestamp NOT NULL, 32 | data text NOT NULL, 33 | parse_data text NOT NULL, 34 | status expression_status NOT NULL, 35 | result int NOT NULL DEFAULT 0, 36 | is_ready boolean NOT NULL DEFAULT false, 37 | 38 | PRIMARY KEY(expression_id), 39 | FOREIGN KEY(agent_id) 40 | REFERENCES agents(agent_id) 41 | ON DELETE SET NULL, 42 | FOREIGN KEY(user_id) 43 | REFERENCES users(user_id) 44 | ON DELETE CASCADE 45 | ); 46 | 47 | CREATE TABLE IF NOT EXISTS operations ( 48 | operation_id int GENERATED ALWAYS AS IDENTITY, 49 | operation_type varchar(1) NOT NULL, 50 | execution_time int NOT NULL DEFAULT 100, 51 | user_id int NOT NULL, 52 | 53 | PRIMARY KEY(operation_id), 54 | CONSTRAINT operation_type_user_id UNIQUE(operation_type, user_id), 55 | FOREIGN KEY(user_id) 56 | REFERENCES users(user_id) 57 | ON DELETE CASCADE 58 | ); 59 | 60 | ALTER TABLE agents ADD COLUMN number_of_active_calculations int NOT NULL DEFAULT 0; -------------------------------------------------------------------------------- /backend/internal/lib/logger/handlers/slogpretty/slogpretty.go: -------------------------------------------------------------------------------- 1 | package slogpretty 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | stdLog "log" 8 | 9 | "log/slog" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | type PrettyHandlerOptions struct { 15 | SlogOpts *slog.HandlerOptions 16 | } 17 | 18 | type PrettyHandler struct { 19 | // opts PrettyHandlerOptions 20 | slog.Handler 21 | l *stdLog.Logger 22 | attrs []slog.Attr 23 | } 24 | 25 | func (opts PrettyHandlerOptions) NewPrettyHandler( 26 | out io.Writer, 27 | ) *PrettyHandler { 28 | h := &PrettyHandler{ 29 | Handler: slog.NewJSONHandler(out, opts.SlogOpts), 30 | l: stdLog.New(out, "", 0), 31 | } 32 | 33 | return h 34 | } 35 | 36 | func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { 37 | level := r.Level.String() + ":" 38 | 39 | switch r.Level { 40 | case slog.LevelDebug: 41 | level = color.MagentaString(level) 42 | case slog.LevelInfo: 43 | level = color.BlueString(level) 44 | case slog.LevelWarn: 45 | level = color.YellowString(level) 46 | case slog.LevelError: 47 | level = color.RedString(level) 48 | } 49 | 50 | fields := make(map[string]interface{}, r.NumAttrs()) 51 | 52 | r.Attrs(func(a slog.Attr) bool { 53 | fields[a.Key] = a.Value.Any() 54 | 55 | return true 56 | }) 57 | 58 | for _, a := range h.attrs { 59 | fields[a.Key] = a.Value.Any() 60 | } 61 | 62 | var b []byte 63 | var err error 64 | 65 | if len(fields) > 0 { 66 | b, err = json.MarshalIndent(fields, "", " ") 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | 72 | timeStr := r.Time.Format("[15:05:05.000]") 73 | msg := color.CyanString(r.Message) 74 | 75 | h.l.Println( 76 | timeStr, 77 | level, 78 | msg, 79 | color.WhiteString(string(b)), 80 | ) 81 | 82 | return nil 83 | } 84 | 85 | func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 86 | return &PrettyHandler{ 87 | Handler: h.Handler, 88 | l: h.l, 89 | attrs: attrs, 90 | } 91 | } 92 | 93 | func (h *PrettyHandler) WithGroup(name string) slog.Handler { 94 | return &PrettyHandler{ 95 | Handler: h.Handler.WithGroup(name), 96 | l: h.l, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /frontend/src/pages/Login/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "src/components/LoginForm/LoginForm"; 2 | import styles from "./LoginPage.module.css"; 3 | import { useState } from "react"; 4 | import { FormVariant } from "src/ts/types"; 5 | import { login, registration } from "src/services/api"; 6 | import { toast } from "react-toastify"; 7 | 8 | export const LoginPage = () => { 9 | const [variant, setVariant] = useState("login"); 10 | const [data, setData] = useState({ 11 | email: "", 12 | password: "", 13 | }); 14 | 15 | const handler = () => { 16 | if (variant === "login") { 17 | login(data).then(() => { 18 | setData({ email: "", password: "" }) 19 | toast.success("Success!"); 20 | }); 21 | } else { 22 | registration(data).then(() => { 23 | toast.success("Success!"); 24 | setVariant("login"); 25 | }) 26 | .catch((err) => { 27 | toast.error(err.response.data.error); 28 | }); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |
35 |
{ 37 | setData({ email: "", password: "" }); 38 | setVariant("login"); 39 | }} 40 | className={styles.activeElement} 41 | style={{ 42 | background: variant === "login" ? "blue" : "white", 43 | color: variant === "login" ? "white" : "black", 44 | }} 45 | > 46 | Login 47 |
48 |
/
49 |
{ 51 | setData({ email: "", password: "" }); 52 | setVariant("reg"); 53 | }} 54 | className={styles.activeElement} 55 | style={{ 56 | background: variant === "reg" ? "blue" : "white", 57 | color: variant === "reg" ? "white" : "black", 58 | }} 59 | > 60 | Registration 61 |
62 |
63 | 69 |
70 | ) 71 | } -------------------------------------------------------------------------------- /backend/internal/lib/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 13 | "github.com/golang-jwt/jwt/v5" 14 | ) 15 | 16 | // NewToken creates new JWT token. 17 | func NewToken(user postgres.User, duration time.Duration) (string, error) { 18 | jwtSecret := os.Getenv("JWT_SECRET") 19 | if jwtSecret == "" { 20 | log.Fatal("JWT_SECRET is not set") 21 | } 22 | 23 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 24 | "uid": user.UserID, 25 | "email": user.Email, 26 | "exp": time.Now().Add(duration).Unix(), 27 | }) 28 | 29 | tokenString, err := token.SignedString([]byte(jwtSecret)) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | return tokenString, nil 35 | } 36 | 37 | func getTokenFromHeader(r *http.Request) (string, error) { 38 | authHeader := r.Header.Get("Authorization") 39 | if authHeader == "" { 40 | return "", fmt.Errorf("authorization header is missing") 41 | } 42 | 43 | // Checks that header starts with "Bearer". 44 | parts := strings.Split(authHeader, " ") 45 | if len(parts) != 2 || parts[0] != "Bearer" { 46 | return "", fmt.Errorf("invalid Authorization header format") 47 | } 48 | 49 | return parts[1], nil // returns token without "Bearer". 50 | } 51 | 52 | func GetUidFromJWT(r *http.Request, secret string) (int32, error) { 53 | jwtToken, err := getTokenFromHeader(r) 54 | if err != nil { 55 | return 0, err 56 | } 57 | 58 | // Parse JWT Token. 59 | token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { 60 | return []byte(secret), nil 61 | }) 62 | if err != nil { 63 | return 0, err 64 | } 65 | 66 | if !token.Valid { 67 | return 0, errors.New("token is invalid") 68 | } 69 | 70 | claims, ok := token.Claims.(jwt.MapClaims) 71 | if !ok { 72 | return 0, errors.New("error in map claims") 73 | } 74 | 75 | userIDFloat, ok := claims["uid"].(float64) 76 | if !ok { 77 | return 0, errors.New("jwt token does not contain uid") 78 | } 79 | 80 | userID := int32(userIDFloat) 81 | 82 | if userID == 0 { 83 | return 0, errors.New("userID == 0") 84 | } 85 | 86 | return userID, nil 87 | } 88 | -------------------------------------------------------------------------------- /backend/internal/rabbitmq/amqp_producer.go: -------------------------------------------------------------------------------- 1 | package rabbitmq 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log/slog" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 9 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 10 | "github.com/streadway/amqp" 11 | ) 12 | 13 | type AMQPProducer struct { 14 | log *slog.Logger 15 | amqpCfg *AMQPConfig 16 | Queue amqp.Queue 17 | Channel *amqp.Channel 18 | } 19 | 20 | // NewAMQPProducer creates new Producer for AMQP protocol. 21 | func NewAMQPProducer(log *slog.Logger, amqpCfg *AMQPConfig, queueName string) (*AMQPProducer, error) { 22 | chProd, err := amqpCfg.conn.Channel() 23 | if err != nil { 24 | log.Error("can't create a channel from RabbitMQ", sl.Err(err)) 25 | return nil, err 26 | } 27 | 28 | queue, err := chProd.QueueDeclare( 29 | queueName, 30 | false, 31 | false, 32 | false, 33 | false, 34 | nil, 35 | ) 36 | if err != nil { 37 | log.Error("can't create a RabbitMQ queue", sl.Err(err)) 38 | return nil, err 39 | } 40 | 41 | return &AMQPProducer{ 42 | log: log, 43 | amqpCfg: amqpCfg, 44 | Queue: queue, 45 | Channel: chProd, 46 | }, nil 47 | } 48 | 49 | // PublishExpressionMessage publishes messages to queue. 50 | func (ap *AMQPProducer) PublishExpressionMessage(msg *messages.ExpressionMessage) error { 51 | jsonData, err := json.Marshal(msg) 52 | if err != nil { 53 | return errors.New("failed to encode message to JSON") 54 | } 55 | 56 | err = ap.Channel.Publish( 57 | "", 58 | ap.Queue.Name, 59 | false, 60 | false, 61 | amqp.Publishing{ 62 | ContentType: "application/json", 63 | Body: jsonData, 64 | }, 65 | ) 66 | 67 | if err != nil { 68 | ap.log.Error("can't publish message to queue", slog.String("queue", ap.Queue.Name), sl.Err(err)) 69 | return errors.New("can't publish message to queue") 70 | } 71 | ap.log.Info("publishing message to queue", slog.String("queue", ap.Queue.Name)) 72 | 73 | return nil 74 | } 75 | 76 | // Reconnect reconnects to AMQP instance. 77 | func (ap *AMQPProducer) Reconnect() (*AMQPProducer, error) { 78 | ap.Close() 79 | 80 | return NewAMQPProducer(ap.log, ap.amqpCfg, ap.Queue.Name) 81 | } 82 | 83 | // Close closes Producer channel. 84 | func (ap *AMQPProducer) Close() { 85 | ap.Channel.Close() 86 | } 87 | -------------------------------------------------------------------------------- /sql/queries/expressions.sql: -------------------------------------------------------------------------------- 1 | -- name: CreateExpression :one 2 | INSERT INTO expressions 3 | (created_at, updated_at, data, parse_data, status, user_id) 4 | VALUES 5 | ($1, $2, $3, $4, $5, $6) 6 | RETURNING 7 | expression_id, user_id, agent_id, 8 | created_at, updated_at, data, parse_data, 9 | status, result, is_ready; 10 | 11 | -- name: GetExpressions :many 12 | SELECT 13 | expression_id, user_id, agent_id, 14 | created_at, updated_at, data, parse_data, 15 | status, result, is_ready 16 | FROM expressions 17 | WHERE user_id = $1 18 | ORDER BY created_at DESC; 19 | 20 | -- name: GetExpressionByID :one 21 | SELECT 22 | expression_id, user_id, agent_id, 23 | created_at, updated_at, data, parse_data, 24 | status, result, is_ready 25 | FROM expressions 26 | WHERE expression_id = $1; 27 | 28 | -- name: UpdateExpressionParseData :exec 29 | UPDATE expressions 30 | SET parse_data = $1 31 | WHERE expression_id = $2; 32 | 33 | -- name: MakeExpressionReady :exec 34 | UPDATE expressions 35 | SET parse_data = $1, result = $2, updated_at = $3, is_ready = True, status = 'result' 36 | WHERE expression_id = $4; 37 | 38 | -- name: UpdateExpressionStatus :exec 39 | UPDATE expressions 40 | SET status = $1 41 | WHERE expression_id = $2; 42 | 43 | -- name: GetComputingExpressions :many 44 | SELECT 45 | expression_id, user_id, agent_id, 46 | created_at, updated_at, data, parse_data, 47 | status, result, is_ready 48 | FROM expressions 49 | WHERE status IN ('ready_for_computation', 'computing', 'terminated') 50 | ORDER BY created_at DESC; 51 | 52 | -- name: MakeExpressionsTerminated :exec 53 | UPDATE expressions 54 | SET status = 'terminated' 55 | WHERE agent_id = $1 AND is_ready = false; 56 | 57 | -- name: GetTerminatedExpressions :many 58 | SELECT 59 | expression_id, user_id, agent_id, 60 | created_at, updated_at, data, parse_data, 61 | status, result, is_ready 62 | FROM expressions 63 | WHERE status = 'terminated' 64 | ORDER BY created_at DESC; 65 | 66 | -- name: AssignExpressionToAgent :exec 67 | UPDATE expressions 68 | SET agent_id = $1 69 | WHERE expression_id = $2; 70 | 71 | -- name: GetExpressionWithStatusComputing :many 72 | SELECT 73 | expression_id, user_id, agent_id, 74 | created_at, updated_at, data, parse_data, 75 | status, result, is_ready 76 | FROM expressions 77 | WHERE status = 'computing' 78 | ORDER BY created_at DESC; -------------------------------------------------------------------------------- /backend/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/ilyakaznacheev/cleanenv" 9 | ) 10 | 11 | type Config struct { 12 | Env string `yaml:"env" env:"ENV" env-default:"local"` 13 | InactiveTimeForAgent int32 `yaml:"inactive_time_for_agent" env-default:"200"` 14 | TimeForPing int32 `yaml:"time_for_ping" end-default:"100"` 15 | TokenTTL time.Duration `yaml:"tokenTTL" env-default:"1h"` 16 | JWTSecret string `env:"JWT_SECRET" env-required:"true"` 17 | GRPCServer `yaml:"grpc_server" env-required:"true"` 18 | DatabaseInstance `yaml:"database_instance" env-required:"true"` 19 | RabbitQueue `yaml:"rabbit_queue" env-required:"true"` 20 | HTTPServer `yaml:"http_server" env-required:"true"` 21 | } 22 | 23 | type HTTPServer struct { 24 | Address string `yaml:"address" env-default:"localhost:8080"` 25 | Timeout time.Duration `yaml:"timeout" env-default:"4s"` 26 | IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s"` 27 | } 28 | 29 | type RabbitQueue struct { 30 | RabbitMQURL string `yaml:"rabbitmq_url" env-default:"amqp://guest:guest@localhost:5672/"` 31 | QueueForExpressionsToAgents string `yaml:"queue_for_expressions_to_agents" env-required:"true"` 32 | QueueForResultsFromAgents string `yaml:"queue_for_results_from_agents" env-required:"true"` 33 | } 34 | 35 | type DatabaseInstance struct { 36 | StorageURL string `yaml:"storage_url" env-default:"postgres://postgres:postgres@localhost:5432/DAEC?sslmode=disable"` 37 | GooseMigrationDir string `yaml:"goose_migration_dir" env:"GOOSE_MIGRATION_DIR" env-required:"true"` 38 | } 39 | 40 | type GRPCServer struct { 41 | Address string `yaml:"address" env-default:"localhost:44044"` 42 | GRPCClientConnectionString string `yaml:"grpc_client_connection_string" env-default:"auth:44044"` 43 | } 44 | 45 | func MustLoad() *Config { 46 | configPath := os.Getenv("CONFIG_PATH") 47 | if configPath == "" { 48 | log.Fatal("CONFIG_PATH is not set") 49 | } 50 | 51 | // check if file exists 52 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 53 | log.Fatalf("config file does not exist: %s", configPath) 54 | } 55 | 56 | var cfg Config 57 | 58 | if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { 59 | 60 | log.Fatalf("cannot read config: %s", err) 61 | } 62 | 63 | return &cfg 64 | } 65 | -------------------------------------------------------------------------------- /backend/internal/http-server/handlers/operation.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/jwt" 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 12 | ) 13 | 14 | // HandlerGetOperations is a http.Handler to get all operations from storage. 15 | func HandlerGetOperations(log *slog.Logger, dbCfg *storage.Storage, secret string) http.HandlerFunc { 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | const fn = "hadlers.HandlerGetOperations" 18 | 19 | log := log.With( 20 | slog.String("fn", fn), 21 | ) 22 | 23 | userID, err := jwt.GetUidFromJWT(r, secret) 24 | if err != nil { 25 | respondWithError(log, w, 403, "Status Forbidden") 26 | return 27 | } 28 | 29 | operations, err := dbCfg.Queries.GetOperations(r.Context(), userID) 30 | if err != nil { 31 | respondWithError(log, w, 400, fmt.Sprintf("can't get operations: %v", err)) 32 | return 33 | } 34 | 35 | respondWithJson(log, w, 200, postgres.DatabaseOperationsToOperations(operations)) 36 | } 37 | } 38 | 39 | // HandlerUpdateOperation is a http.Handler to update execution time of the certain operation type. 40 | func HandlerUpdateOperation(log *slog.Logger, dbCfg *storage.Storage, secret string) http.HandlerFunc { 41 | return func(w http.ResponseWriter, r *http.Request) { 42 | const fn = "handlers.HandlerUpdateOperation" 43 | 44 | log := log.With( 45 | slog.String("fn", fn), 46 | ) 47 | 48 | userID, err := jwt.GetUidFromJWT(r, secret) 49 | if err != nil { 50 | respondWithError(log, w, 403, "Status Forbidden") 51 | return 52 | } 53 | 54 | type parametrs struct { 55 | OperationType string `json:"operation_type"` 56 | ExecutionTime int32 `json:"execution_time"` 57 | } 58 | 59 | decoder := json.NewDecoder(r.Body) 60 | params := parametrs{} 61 | err = decoder.Decode(¶ms) 62 | if err != nil { 63 | respondWithError(log, w, 400, fmt.Sprintf("error parsing JSON: %v", err)) 64 | } 65 | 66 | operation, err := dbCfg.Queries.UpdateOperationTime(r.Context(), postgres.UpdateOperationTimeParams{ 67 | OperationType: params.OperationType, 68 | ExecutionTime: params.ExecutionTime, 69 | UserID: userID, 70 | }) 71 | 72 | if err != nil { 73 | respondWithError(log, w, 400, fmt.Sprintf("can't update operation: %v", err)) 74 | return 75 | } 76 | 77 | respondWithJson(log, w, 200, postgres.DatabaseOperationToOperation(operation)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /backend/internal/http-server/handlers/user.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | daecv1 "github.com/Prrromanssss/DAEC-fullstack/internal/protos/gen/go/daec" 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 11 | ) 12 | 13 | // HandlerLoginUser is a http.Handler to login user. 14 | func HandlerLoginUser( 15 | log *slog.Logger, 16 | dbCfg *storage.Storage, 17 | grpcClient daecv1.AuthClient, 18 | ) http.HandlerFunc { 19 | return func(w http.ResponseWriter, r *http.Request) { 20 | const fn = "handlers.HandlerLoginUser" 21 | 22 | log := log.With( 23 | slog.String("fn", fn), 24 | ) 25 | 26 | type parametrs struct { 27 | Email string `json:"email"` 28 | Password string `json:"password"` 29 | } 30 | 31 | decoder := json.NewDecoder(r.Body) 32 | params := parametrs{} 33 | err := decoder.Decode(¶ms) 34 | if err != nil { 35 | respondWithError(log, w, 400, fmt.Sprintf("error parsing JSON: %v", err)) 36 | return 37 | } 38 | 39 | loginResponse, err := grpcClient.Login(r.Context(), &daecv1.LoginRequest{ 40 | Email: params.Email, 41 | Password: params.Password, 42 | }) 43 | if err != nil { 44 | respondWithError(log, w, 400, fmt.Sprintf("can't login user: %v", err)) 45 | return 46 | } 47 | 48 | respondWithJson(log, w, 200, loginResponse) 49 | } 50 | } 51 | 52 | // HandlerRegisterNewUser is a http.Handler to register new user. 53 | func HandlerRegisterNewUser( 54 | log *slog.Logger, 55 | dbCfg *storage.Storage, 56 | grpcClient daecv1.AuthClient, 57 | ) http.HandlerFunc { 58 | return func(w http.ResponseWriter, r *http.Request) { 59 | const fn = "handlers.HandlerLoginUser" 60 | 61 | log := log.With( 62 | slog.String("fn", fn), 63 | ) 64 | 65 | type parametrs struct { 66 | Email string `json:"email"` 67 | Password string `json:"password"` 68 | } 69 | 70 | decoder := json.NewDecoder(r.Body) 71 | params := parametrs{} 72 | err := decoder.Decode(¶ms) 73 | if err != nil { 74 | respondWithError(log, w, 400, fmt.Sprintf("error parsing JSON: %v", err)) 75 | return 76 | } 77 | 78 | registerResponse, err := grpcClient.Register(r.Context(), &daecv1.RegisterRequest{ 79 | Email: params.Email, 80 | Password: params.Password, 81 | }) 82 | if err != nil { 83 | respondWithError(log, w, 400, fmt.Sprintf("can't register new user: %v", err)) 84 | return 85 | } 86 | 87 | err = dbCfg.Queries.NewOperationsForUser(r.Context(), int32(registerResponse.UserId)) 88 | if err != nil { 89 | respondWithError(log, w, 400, fmt.Sprintf("can't create new operations for user: %v", err)) 90 | return 91 | } 92 | 93 | respondWithJson(log, w, 200, registerResponse) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /backend/internal/grpc/auth/server.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | daecv1 "github.com/Prrromanssss/DAEC-fullstack/internal/protos/gen/go/daec" 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/services/auth" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | type Auth interface { 15 | Login( 16 | ctx context.Context, 17 | email string, 18 | password string, 19 | ) (token string, err error) 20 | RegisterNewUser( 21 | ctx context.Context, 22 | email string, 23 | password string, 24 | ) (userID int64, err error) 25 | } 26 | 27 | type serverAPI struct { 28 | daecv1.UnimplementedAuthServer 29 | auth Auth 30 | } 31 | 32 | func Register(gRPC *grpc.Server, auth Auth) { 33 | daecv1.RegisterAuthServer(gRPC, &serverAPI{auth: auth}) 34 | } 35 | 36 | func (s *serverAPI) Login( 37 | ctx context.Context, 38 | req *daecv1.LoginRequest, 39 | ) (*daecv1.LoginResponse, error) { 40 | if err := validateLogin(req); err != nil { 41 | return nil, err 42 | } 43 | 44 | token, err := s.auth.Login(ctx, req.GetEmail(), req.GetPassword()) 45 | if err != nil { 46 | if errors.Is(err, auth.ErrInvalidCredentials) { 47 | return nil, status.Error(codes.InvalidArgument, "invalid email or password") 48 | } 49 | return nil, status.Error(codes.Internal, "internal error") 50 | } 51 | 52 | return &daecv1.LoginResponse{ 53 | Token: token, 54 | }, nil 55 | } 56 | 57 | func (s *serverAPI) Register( 58 | ctx context.Context, 59 | req *daecv1.RegisterRequest, 60 | ) (*daecv1.RegisterResponse, error) { 61 | if err := validateRegister(req); err != nil { 62 | return nil, err 63 | } 64 | 65 | userID, err := s.auth.RegisterNewUser(ctx, req.GetEmail(), req.GetPassword()) 66 | if err != nil { 67 | if errors.Is(err, auth.ErrUserExists) { 68 | return nil, status.Error(codes.AlreadyExists, "user already exists") 69 | } 70 | return nil, status.Error(codes.Internal, "internal error") 71 | } 72 | 73 | return &daecv1.RegisterResponse{ 74 | UserId: userID, 75 | }, nil 76 | } 77 | 78 | func validateLogin(req *daecv1.LoginRequest) error { 79 | if req.GetEmail() == "" { 80 | return status.Error(codes.InvalidArgument, "email is required") 81 | } 82 | 83 | if req.GetPassword() == "" { 84 | return status.Error(codes.InvalidArgument, "password is required") 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func validateRegister(req *daecv1.RegisterRequest) error { 91 | if req.GetEmail() == "" { 92 | return status.Error(codes.InvalidArgument, "email is required") 93 | } 94 | 95 | if req.GetPassword() == "" { 96 | return status.Error(codes.InvalidArgument, "password is required") 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /backend/internal/storage/postgres/operations.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | // source: operations.sql 5 | 6 | package postgres 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const getOperationTimeByType = `-- name: GetOperationTimeByType :one 13 | SELECT execution_time 14 | FROM operations 15 | WHERE operation_type = $1 AND user_id = $2 16 | ` 17 | 18 | type GetOperationTimeByTypeParams struct { 19 | OperationType string 20 | UserID int32 21 | } 22 | 23 | func (q *Queries) GetOperationTimeByType(ctx context.Context, arg GetOperationTimeByTypeParams) (int32, error) { 24 | row := q.db.QueryRowContext(ctx, getOperationTimeByType, arg.OperationType, arg.UserID) 25 | var execution_time int32 26 | err := row.Scan(&execution_time) 27 | return execution_time, err 28 | } 29 | 30 | const getOperations = `-- name: GetOperations :many 31 | SELECT 32 | operation_id, operation_type, execution_time, user_id 33 | FROM operations 34 | WHERE user_id = $1 35 | ORDER BY operation_type DESC 36 | ` 37 | 38 | func (q *Queries) GetOperations(ctx context.Context, userID int32) ([]Operation, error) { 39 | rows, err := q.db.QueryContext(ctx, getOperations, userID) 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer rows.Close() 44 | var items []Operation 45 | for rows.Next() { 46 | var i Operation 47 | if err := rows.Scan( 48 | &i.OperationID, 49 | &i.OperationType, 50 | &i.ExecutionTime, 51 | &i.UserID, 52 | ); err != nil { 53 | return nil, err 54 | } 55 | items = append(items, i) 56 | } 57 | if err := rows.Close(); err != nil { 58 | return nil, err 59 | } 60 | if err := rows.Err(); err != nil { 61 | return nil, err 62 | } 63 | return items, nil 64 | } 65 | 66 | const newOperationsForUser = `-- name: NewOperationsForUser :exec 67 | INSERT INTO operations (operation_type, user_id) VALUES 68 | ('+', $1), 69 | ('-', $1), 70 | ('*', $1), 71 | ('/', $1) 72 | ` 73 | 74 | func (q *Queries) NewOperationsForUser(ctx context.Context, userID int32) error { 75 | _, err := q.db.ExecContext(ctx, newOperationsForUser, userID) 76 | return err 77 | } 78 | 79 | const updateOperationTime = `-- name: UpdateOperationTime :one 80 | UPDATE operations 81 | SET execution_time = $1 82 | WHERE operation_type = $2 AND user_id = $3 83 | RETURNING operation_id, operation_type, execution_time, user_id 84 | ` 85 | 86 | type UpdateOperationTimeParams struct { 87 | ExecutionTime int32 88 | OperationType string 89 | UserID int32 90 | } 91 | 92 | func (q *Queries) UpdateOperationTime(ctx context.Context, arg UpdateOperationTimeParams) (Operation, error) { 93 | row := q.db.QueryRowContext(ctx, updateOperationTime, arg.ExecutionTime, arg.OperationType, arg.UserID) 94 | var i Operation 95 | err := row.Scan( 96 | &i.OperationID, 97 | &i.OperationType, 98 | &i.ExecutionTime, 99 | &i.UserID, 100 | ) 101 | return i, err 102 | } 103 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/parser/tokens.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 11 | ) 12 | 13 | // GetTokens gets tokens " " from parseExpression. 14 | func GetTokens(log *slog.Logger, parseExpression string) []string { 15 | const fn = "parser.GetTokens" 16 | 17 | res := make([]string, 0) 18 | ind := 0 19 | tokens := strings.Split(parseExpression, " ") 20 | 21 | log.Info("finding tokens", slog.String("fn", fn), slog.Any("tokens", tokens)) 22 | 23 | for ind+2 < len(tokens) { 24 | if IsNumber(string(tokens[ind])) && 25 | IsNumber(string(tokens[ind+1])) && 26 | !IsNumber(string(tokens[ind+2])) { 27 | res = append(res, fmt.Sprint(string(tokens[ind]), " ", string(tokens[ind+1]), " ", string(tokens[ind+2]))) 28 | ind += 2 29 | } 30 | ind++ 31 | } 32 | 33 | return res 34 | } 35 | 36 | // InsertResultToToken inserts result into parseExpression to the place of the token. 37 | // Returns new token if the insertion was at the beginning of the parseExpression. 38 | func InsertResultToToken(parseExpression, token string, result int) (messages.ResultAndTokenMessage, error) { 39 | ind := 0 40 | tokens := strings.Split(parseExpression, " ") 41 | res := make([]string, 0) 42 | sourceTokens := strings.Split(token, " ") 43 | newToken := "" 44 | isTokenFind := false 45 | 46 | if len(tokens) == 3 { 47 | return messages.ResultAndTokenMessage{Result: fmt.Sprint(result)}, nil 48 | } 49 | 50 | for ind+2 < len(tokens) { 51 | if string(tokens[ind]) == sourceTokens[0] && 52 | string(tokens[ind+1]) == sourceTokens[1] && 53 | string(tokens[ind+2]) == sourceTokens[2] { 54 | 55 | res = append(res, strconv.Itoa(result)) 56 | 57 | isTokenFind = true 58 | if ind > 0 && ind+3 >= len(tokens) { 59 | return messages.ResultAndTokenMessage{}, errors.New("invalid expression") 60 | } 61 | if ind > 0 && IsNumber(string(tokens[ind-1])) && !IsNumber(string(tokens[ind+3])) { 62 | newToken = fmt.Sprint(tokens[ind-1], " ", result, " ", tokens[ind+3]) 63 | } else if ind > 0 && ind+4 < len(tokens) && !IsNumber(string(tokens[ind-1])) && 64 | IsNumber(string(tokens[ind+3])) && 65 | !IsNumber(string(tokens[ind+4])) { 66 | newToken = fmt.Sprint(result, " ", tokens[ind+3], " ", tokens[ind+4]) 67 | } else if ind == 0 && ind+4 < len(tokens) && IsNumber(string(tokens[ind+3])) && 68 | !IsNumber(string(tokens[ind+4])) { 69 | newToken = fmt.Sprint(result, " ", tokens[ind+3], " ", tokens[ind+4]) 70 | } 71 | 72 | ind += 3 73 | break 74 | } else { 75 | res = append(res, tokens[ind]) 76 | } 77 | ind++ 78 | } 79 | 80 | for ind < len(tokens) { 81 | res = append(res, tokens[ind]) 82 | ind++ 83 | } 84 | 85 | if !isTokenFind { 86 | return messages.ResultAndTokenMessage{}, errors.New("can't find token") 87 | } 88 | 89 | return messages.ResultAndTokenMessage{ 90 | Result: strings.Join(res, " "), 91 | Token: newToken, 92 | }, nil 93 | } 94 | -------------------------------------------------------------------------------- /backend/internal/storage/postgres/model_transformers.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | type ExpressionTransformed struct { 9 | ExpressionID int32 `json:"expression_id"` 10 | UserID int32 `json:"user_id"` 11 | AgentID sql.NullInt32 `json:"agent_id"` 12 | CreatedAt time.Time `json:"created_at"` 13 | UpdatedAt time.Time `json:"updated_at"` 14 | Data string `json:"data"` 15 | ParseData string `json:"parse_data"` 16 | Status ExpressionStatus `json:"status"` 17 | Result int32 `json:"result"` 18 | IsReady bool `json:"is_ready"` 19 | } 20 | 21 | func DatabaseExpressionToExpression(dbExpr Expression) ExpressionTransformed { 22 | return ExpressionTransformed(dbExpr) 23 | } 24 | 25 | func DatabaseExpressionsToExpressions(dbExprs []Expression) []ExpressionTransformed { 26 | exprs := []ExpressionTransformed{} 27 | for _, dbExpr := range dbExprs { 28 | exprs = append(exprs, DatabaseExpressionToExpression(dbExpr)) 29 | } 30 | return exprs 31 | } 32 | 33 | type OperationTransformed struct { 34 | OperationID int32 `json:"operation_id"` 35 | OperationType string `json:"operation_type"` 36 | ExecutionTime int32 `json:"execution_time"` 37 | UserID int32 `json:"user_id"` 38 | } 39 | 40 | func DatabaseOperationToOperation(dbOper Operation) OperationTransformed { 41 | return OperationTransformed(dbOper) 42 | } 43 | 44 | func DatabaseOperationsToOperations(dbOpers []Operation) []OperationTransformed { 45 | opers := []OperationTransformed{} 46 | for _, dbOper := range dbOpers { 47 | opers = append(opers, DatabaseOperationToOperation(dbOper)) 48 | } 49 | return opers 50 | } 51 | 52 | type AgentTransformed struct { 53 | AgentID int32 `json:"agent_id"` 54 | NumberOfParallelCalculations int32 `json:"number_of_parallel_calculations"` 55 | LastPing time.Time `json:"last_ping"` 56 | Status AgentStatus `json:"status"` 57 | CreatedAt time.Time `json:"created_at"` 58 | NumberOfActiveCalculations int32 `json:"number_of_active_calculations"` 59 | } 60 | 61 | func DatabaseAgentToAgent(dbAgent Agent) AgentTransformed { 62 | return AgentTransformed(dbAgent) 63 | } 64 | 65 | func DatabaseAgentsToAgents(dbAgents []Agent) []AgentTransformed { 66 | agents := []AgentTransformed{} 67 | for _, dbAgent := range dbAgents { 68 | agents = append(agents, DatabaseAgentToAgent(dbAgent)) 69 | } 70 | return agents 71 | } 72 | 73 | type UserTransformed struct { 74 | UserID int32 `json:"user_id"` 75 | Email string `json:"email"` 76 | PasswordHash []byte `json:"password_hash"` 77 | } 78 | 79 | func DatabaseUserToUser(dbUser User) UserTransformed { 80 | return UserTransformed(dbUser) 81 | } 82 | 83 | func DatabaseUsersToUsers(dbUsers []User) []UserTransformed { 84 | users := []UserTransformed{} 85 | for _, dbUser := range dbUsers { 86 | users = append(users, DatabaseUserToUser(dbUser)) 87 | } 88 | return users 89 | } 90 | -------------------------------------------------------------------------------- /backend/internal/http-server/handlers/expression.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/brokers" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/jwt" 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator" 14 | 15 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator/parser" 16 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 17 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 18 | ) 19 | 20 | // HandlerCreateExpression is a http.Handler to create new expression. 21 | func HandlerCreateExpression( 22 | log *slog.Logger, 23 | dbCfg *storage.Storage, 24 | secret string, 25 | orc *orchestrator.Orchestrator, 26 | producer brokers.Producer, 27 | ) http.HandlerFunc { 28 | return func(w http.ResponseWriter, r *http.Request) { 29 | const fn = "handlers.HandlerCreateExpression" 30 | 31 | log := log.With( 32 | slog.String("fn", fn), 33 | ) 34 | 35 | userID, err := jwt.GetUidFromJWT(r, secret) 36 | if err != nil { 37 | respondWithError(log, w, 403, "Status Forbidden") 38 | return 39 | } 40 | 41 | type parametrs struct { 42 | Data string `json:"data"` 43 | } 44 | 45 | decoder := json.NewDecoder(r.Body) 46 | params := parametrs{} 47 | err = decoder.Decode(¶ms) 48 | if err != nil { 49 | respondWithError(log, w, 400, fmt.Sprintf("error parsing JSON: %v", err)) 50 | return 51 | } 52 | 53 | parseData, err := parser.ParseExpression(params.Data) 54 | if err != nil { 55 | respondWithError(log, w, 400, fmt.Sprintf("error parsing expression: %v", err)) 56 | return 57 | } 58 | 59 | expression, err := dbCfg.Queries.CreateExpression(r.Context(), 60 | postgres.CreateExpressionParams{ 61 | CreatedAt: time.Now().UTC(), 62 | UpdatedAt: time.Now().UTC(), 63 | Data: params.Data, 64 | ParseData: parseData, 65 | Status: "ready_for_computation", 66 | UserID: userID, 67 | }) 68 | if err != nil { 69 | respondWithError(log, w, 400, fmt.Sprintf("can't create expression: %v", err)) 70 | return 71 | } 72 | 73 | msgToQueue := messages.ExpressionMessage{ 74 | ExpressionID: expression.ExpressionID, 75 | Expression: parseData, 76 | UserID: userID, 77 | } 78 | 79 | orc.AddTask(msgToQueue, producer) 80 | 81 | log.Info("send message to orchestrator") 82 | 83 | respondWithJson(log, w, 201, postgres.DatabaseExpressionToExpression(expression)) 84 | } 85 | } 86 | 87 | // HandlerGetExpressions is a http.Handler to get all expressions from storage. 88 | func HandlerGetExpressions(log *slog.Logger, dbCfg *storage.Storage, secret string) http.HandlerFunc { 89 | return func(w http.ResponseWriter, r *http.Request) { 90 | const fn = "handlers.HandlerCreateExpression" 91 | 92 | log := log.With( 93 | slog.String("fn", fn), 94 | ) 95 | 96 | userID, err := jwt.GetUidFromJWT(r, secret) 97 | if err != nil { 98 | respondWithError(log, w, 403, "Status Forbidden") 99 | return 100 | } 101 | 102 | expressions, err := dbCfg.Queries.GetExpressions(r.Context(), userID) 103 | if err != nil { 104 | respondWithError(log, w, 400, fmt.Sprintf("Couldn't get expressions: %v", err)) 105 | return 106 | } 107 | 108 | respondWithJson(log, w, 200, postgres.DatabaseExpressionsToExpressions(expressions)) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /backend/internal/storage/postgres/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | 5 | package postgres 6 | 7 | import ( 8 | "database/sql" 9 | "database/sql/driver" 10 | "fmt" 11 | "time" 12 | ) 13 | 14 | type AgentStatus string 15 | 16 | const ( 17 | AgentStatusRunning AgentStatus = "running" 18 | AgentStatusWaiting AgentStatus = "waiting" 19 | AgentStatusSleeping AgentStatus = "sleeping" 20 | AgentStatusTerminated AgentStatus = "terminated" 21 | ) 22 | 23 | func (e *AgentStatus) Scan(src interface{}) error { 24 | switch s := src.(type) { 25 | case []byte: 26 | *e = AgentStatus(s) 27 | case string: 28 | *e = AgentStatus(s) 29 | default: 30 | return fmt.Errorf("unsupported scan type for AgentStatus: %T", src) 31 | } 32 | return nil 33 | } 34 | 35 | type NullAgentStatus struct { 36 | AgentStatus AgentStatus 37 | Valid bool // Valid is true if AgentStatus is not NULL 38 | } 39 | 40 | // Scan implements the Scanner interface. 41 | func (ns *NullAgentStatus) Scan(value interface{}) error { 42 | if value == nil { 43 | ns.AgentStatus, ns.Valid = "", false 44 | return nil 45 | } 46 | ns.Valid = true 47 | return ns.AgentStatus.Scan(value) 48 | } 49 | 50 | // Value implements the driver Valuer interface. 51 | func (ns NullAgentStatus) Value() (driver.Value, error) { 52 | if !ns.Valid { 53 | return nil, nil 54 | } 55 | return string(ns.AgentStatus), nil 56 | } 57 | 58 | type ExpressionStatus string 59 | 60 | const ( 61 | ExpressionStatusReadyForComputation ExpressionStatus = "ready_for_computation" 62 | ExpressionStatusComputing ExpressionStatus = "computing" 63 | ExpressionStatusResult ExpressionStatus = "result" 64 | ExpressionStatusTerminated ExpressionStatus = "terminated" 65 | ) 66 | 67 | func (e *ExpressionStatus) Scan(src interface{}) error { 68 | switch s := src.(type) { 69 | case []byte: 70 | *e = ExpressionStatus(s) 71 | case string: 72 | *e = ExpressionStatus(s) 73 | default: 74 | return fmt.Errorf("unsupported scan type for ExpressionStatus: %T", src) 75 | } 76 | return nil 77 | } 78 | 79 | type NullExpressionStatus struct { 80 | ExpressionStatus ExpressionStatus 81 | Valid bool // Valid is true if ExpressionStatus is not NULL 82 | } 83 | 84 | // Scan implements the Scanner interface. 85 | func (ns *NullExpressionStatus) Scan(value interface{}) error { 86 | if value == nil { 87 | ns.ExpressionStatus, ns.Valid = "", false 88 | return nil 89 | } 90 | ns.Valid = true 91 | return ns.ExpressionStatus.Scan(value) 92 | } 93 | 94 | // Value implements the driver Valuer interface. 95 | func (ns NullExpressionStatus) Value() (driver.Value, error) { 96 | if !ns.Valid { 97 | return nil, nil 98 | } 99 | return string(ns.ExpressionStatus), nil 100 | } 101 | 102 | type Agent struct { 103 | AgentID int32 104 | NumberOfParallelCalculations int32 105 | LastPing time.Time 106 | Status AgentStatus 107 | CreatedAt time.Time 108 | NumberOfActiveCalculations int32 109 | } 110 | 111 | type Expression struct { 112 | ExpressionID int32 113 | UserID int32 114 | AgentID sql.NullInt32 115 | CreatedAt time.Time 116 | UpdatedAt time.Time 117 | Data string 118 | ParseData string 119 | Status ExpressionStatus 120 | Result int32 121 | IsReady bool 122 | } 123 | 124 | type Operation struct { 125 | OperationID int32 126 | OperationType string 127 | ExecutionTime int32 128 | UserID int32 129 | } 130 | 131 | type User struct { 132 | UserID int32 133 | Email string 134 | PasswordHash []byte 135 | } 136 | -------------------------------------------------------------------------------- /backend/internal/app/agent/app.go: -------------------------------------------------------------------------------- 1 | package agentapp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "sync" 8 | "time" 9 | 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/agent" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/config" 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/brokers" 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 14 | "github.com/Prrromanssss/DAEC-fullstack/internal/rabbitmq" 15 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 16 | "github.com/streadway/amqp" 17 | ) 18 | 19 | type App struct { 20 | log *slog.Logger 21 | AgentApp *agent.Agent 22 | mu *sync.Mutex 23 | TimeForPing int32 24 | amqpConfig rabbitmq.AMQPConfig 25 | Producer brokers.Producer 26 | Consumer brokers.Consumer 27 | } 28 | 29 | // MustRun runs Agent and panics if any error occurs. 30 | func (a *App) MustRun(ctx context.Context) { 31 | if err := a.Run(ctx); err != nil { 32 | panic(err) 33 | } 34 | } 35 | 36 | // New creates new Agent app. 37 | func New( 38 | log *slog.Logger, 39 | cfg *config.Config, 40 | dbCfg *storage.Storage, 41 | cancel context.CancelFunc, 42 | ) (*App, error) { 43 | amqpCfg, err := rabbitmq.NewAMQPConfig(log, cfg.RabbitMQURL) 44 | if err != nil { 45 | log.Error("can't create NewAMQPConfig", sl.Err(err)) 46 | return nil, err 47 | } 48 | 49 | producer, err := rabbitmq.NewAMQPProducer(log, amqpCfg, cfg.QueueForResultsFromAgents) 50 | if err != nil { 51 | log.Error("can't create NewAMQPProducer", sl.Err(err)) 52 | return nil, err 53 | } 54 | 55 | consumer, err := rabbitmq.NewAMQPConsumer(log, amqpCfg, cfg.QueueForExpressionsToAgents) 56 | if err != nil { 57 | log.Error("can't create NewAMQPConsumer", sl.Err(err)) 58 | return nil, err 59 | } 60 | 61 | ag, err := agent.NewAgent( 62 | log, 63 | dbCfg, 64 | cancel, 65 | ) 66 | if err != nil { 67 | log.Error("can't create agent", sl.Err(err)) 68 | return nil, err 69 | } 70 | 71 | return &App{ 72 | log: log, 73 | AgentApp: ag, 74 | mu: &sync.Mutex{}, 75 | TimeForPing: cfg.TimeForPing, 76 | amqpConfig: *amqpCfg, 77 | Producer: producer, 78 | Consumer: consumer, 79 | }, nil 80 | } 81 | 82 | // Run gets messages from SimpleComputers, handle these messages, 83 | // sends pings to Agent Agregator. 84 | func (a *App) Run(ctx context.Context) error { 85 | defer func() { 86 | a.amqpConfig.Close() 87 | a.Producer.Close() 88 | a.Consumer.Close() 89 | }() 90 | 91 | go func() { 92 | for msgFromOrchestrator := range a.Consumer.GetMessages() { 93 | a.mu.Lock() 94 | if a.AgentApp.NumberOfActiveCalculations >= a.AgentApp.NumberOfParallelCalculations { 95 | a.mu.Unlock() 96 | err := msgFromOrchestrator.Nack(false, true) 97 | if err != nil { 98 | a.log.Error("can't nack message", sl.Err(err)) 99 | return 100 | } 101 | continue // skip the processing of this message and move on to the next one. 102 | } 103 | a.AgentApp.NumberOfActiveCalculations++ 104 | a.mu.Unlock() 105 | 106 | go func(msgFromOrchestrator amqp.Delivery) { 107 | a.AgentApp.ConsumeMessageFromOrchestrator(ctx, msgFromOrchestrator) 108 | }(msgFromOrchestrator) 109 | } 110 | }() 111 | 112 | ticker := time.NewTicker(time.Duration(a.TimeForPing) * time.Second) 113 | defer ticker.Stop() 114 | 115 | for { 116 | select { 117 | case result := <-a.AgentApp.SimpleComputers: 118 | go a.AgentApp.ConsumeMessageFromComputers(ctx, result, a.Producer) 119 | case <-ctx.Done(): 120 | a.AgentApp.Terminate() 121 | return fmt.Errorf("agent terminated") 122 | case <-ticker.C: 123 | a.AgentApp.Ping(a.Producer) 124 | } 125 | } 126 | } 127 | 128 | // Stop stops Agent app. 129 | func (a *App) Stop(ctx context.Context) { 130 | a.AgentApp.Terminate() 131 | } 132 | -------------------------------------------------------------------------------- /backend/internal/services/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "time" 9 | 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/jwt" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 14 | "golang.org/x/crypto/bcrypt" 15 | ) 16 | 17 | type Auth struct { 18 | log *slog.Logger 19 | usrSaver UserSaver 20 | usrProvider UserProvider 21 | tokenTTL time.Duration 22 | } 23 | 24 | type UserSaver interface { 25 | SaveUser( 26 | ctx context.Context, 27 | email string, 28 | passHash []byte, 29 | ) (uid int32, err error) 30 | } 31 | 32 | type UserProvider interface { 33 | User(ctx context.Context, email string) (postgres.User, error) 34 | } 35 | 36 | var ( 37 | ErrInvalidCredentials = errors.New("invalid credentials") 38 | ErrInvalidAppID = errors.New("invalid app id") 39 | ErrUserExists = errors.New("user already exists") 40 | ErrUserNotFound = errors.New("user not found") 41 | ) 42 | 43 | // New returns a new instance of the Auth service. 44 | func New( 45 | log *slog.Logger, 46 | userSaver UserSaver, 47 | userProvider UserProvider, 48 | tokenTTL time.Duration, 49 | ) *Auth { 50 | return &Auth{ 51 | log: log, 52 | usrSaver: userSaver, 53 | usrProvider: userProvider, 54 | tokenTTL: tokenTTL, 55 | } 56 | } 57 | 58 | // Login checks if user with given credentials exists in the system and returns access token. 59 | // 60 | // If user exists, but password is incorrect, returns error. 61 | // If user doesn't exist, returns error. 62 | func (a *Auth) Login( 63 | ctx context.Context, 64 | email string, 65 | password string, 66 | ) (string, error) { 67 | const op = "auth.Login" 68 | 69 | log := a.log.With( 70 | slog.String("op", op), 71 | slog.String("username", email), 72 | ) 73 | 74 | log.Info("attempting to login user") 75 | 76 | user, err := a.usrProvider.User(ctx, email) 77 | if err != nil { 78 | if errors.Is(err, storage.ErrUserNotFound) { 79 | a.log.Warn("user not found", sl.Err(err)) 80 | 81 | return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials) 82 | } 83 | 84 | a.log.Error("failed to get user", sl.Err(err)) 85 | 86 | return "", fmt.Errorf("%s: %w", op, err) 87 | } 88 | 89 | if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)); err != nil { 90 | a.log.Info("invalid credentials", sl.Err(err)) 91 | 92 | return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials) 93 | } 94 | 95 | log.Info("user logged successfully") 96 | 97 | token, err := jwt.NewToken(user, a.tokenTTL) 98 | if err != nil { 99 | a.log.Error("failed to generate token", sl.Err(err)) 100 | 101 | return "", fmt.Errorf("%s: %w", op, err) 102 | } 103 | 104 | return token, nil 105 | } 106 | 107 | // RegisterNewUser registers new user in the system and returns user ID. 108 | // If user with given username already exists, returns error. 109 | func (a *Auth) RegisterNewUser( 110 | ctx context.Context, 111 | email string, 112 | pass string, 113 | ) (int64, error) { 114 | const op = "auth.RegisterNewUser" 115 | 116 | log := a.log.With( 117 | slog.String("op", op), 118 | slog.String("email", email), 119 | ) 120 | 121 | log.Info("registering user") 122 | 123 | passHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) 124 | if err != nil { 125 | log.Error("failed to generate password hash", sl.Err(err)) 126 | 127 | return 0, fmt.Errorf("%s: %w", op, err) 128 | } 129 | 130 | id, err := a.usrSaver.SaveUser(ctx, email, passHash) 131 | if err != nil { 132 | if errors.Is(err, storage.ErrUserExists) { 133 | log.Warn("user already exists", sl.Err(err)) 134 | 135 | return 0, fmt.Errorf("%s: %w", op, ErrUserExists) 136 | } 137 | log.Error("failed to save user", sl.Err(err)) 138 | 139 | return 0, fmt.Errorf("%s: %w", op, err) 140 | } 141 | 142 | log.Info("user registered") 143 | 144 | return int64(id), nil 145 | } 146 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 4 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 5 | github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= 6 | github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= 7 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 8 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 9 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 10 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 11 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 12 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= 16 | github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= 17 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 18 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 19 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 20 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 21 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 22 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 23 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 24 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 25 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 26 | github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= 27 | github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= 28 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 29 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 30 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 31 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 32 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 35 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 37 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 38 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= 39 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= 40 | google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= 41 | google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= 42 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 43 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= 49 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= 50 | -------------------------------------------------------------------------------- /backend/cmd/orchestrator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | orchestratorapp "github.com/Prrromanssss/DAEC-fullstack/internal/app/orchestrator" 12 | daecv1 "github.com/Prrromanssss/DAEC-fullstack/internal/protos/gen/go/daec" 13 | "google.golang.org/grpc" 14 | "google.golang.org/grpc/credentials/insecure" 15 | 16 | "github.com/Prrromanssss/DAEC-fullstack/internal/config" 17 | "github.com/Prrromanssss/DAEC-fullstack/internal/http-server/handlers" 18 | mwlogger "github.com/Prrromanssss/DAEC-fullstack/internal/http-server/middleware/logger" 19 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/setup" 20 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 21 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 22 | 23 | "github.com/go-chi/chi" 24 | "github.com/go-chi/chi/v5/middleware" 25 | "github.com/go-chi/cors" 26 | ) 27 | 28 | func main() { 29 | ctxWithCancel, cancel := context.WithCancel(context.Background()) 30 | defer cancel() 31 | 32 | // Load Config 33 | cfg := config.MustLoad() 34 | 35 | // Configuration Logger 36 | log := setup.SetupLogger(cfg.Env) 37 | log.Info( 38 | "start orchestrator", 39 | slog.String("env", cfg.Env), 40 | slog.String("version", "2"), 41 | ) 42 | 43 | // Configuration Storage 44 | dbCfg := storage.NewStorage(log, cfg.StorageURL) 45 | 46 | // Delete terminated agents 47 | err := dbCfg.Queries.TerminateOldAgents(ctxWithCancel) 48 | if err != nil { 49 | log.Warn("can't delete old agents") 50 | } 51 | 52 | // Configuration Orchestrator 53 | application, err := orchestratorapp.New(log, cfg, dbCfg, cancel) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | go application.MustRun(ctxWithCancel) 59 | 60 | // Configuration gRPC Client 61 | conn, err := grpc.Dial( 62 | cfg.GRPCServer.GRPCClientConnectionString, 63 | grpc.WithTransportCredentials(insecure.NewCredentials()), 64 | ) 65 | if err != nil { 66 | log.Error("could not connect to grpc server", sl.Err(err)) 67 | os.Exit(1) 68 | } 69 | defer conn.Close() 70 | 71 | log.Info("succesfully connect to gRPC server") 72 | 73 | grpcClient := daecv1.NewAuthClient(conn) 74 | 75 | // Configuration HTTP-Server 76 | router := chi.NewRouter() 77 | 78 | router.Use(cors.Handler(cors.Options{ 79 | AllowedOrigins: []string{"https://*", "http://*"}, 80 | AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS"}, 81 | AllowedHeaders: []string{"*"}, 82 | ExposedHeaders: []string{"Link"}, 83 | AllowCredentials: false, 84 | MaxAge: 300, 85 | })) 86 | 87 | v1Router := chi.NewRouter() 88 | 89 | v1Router.Use(middleware.RequestID) 90 | v1Router.Use(mwlogger.New(log)) 91 | v1Router.Use(middleware.URLFormat) 92 | 93 | // Expression endpoints 94 | v1Router.Post("/expressions", handlers.HandlerCreateExpression( 95 | log, 96 | dbCfg, 97 | cfg.JWTSecret, 98 | application.OrchestratorApp, 99 | application.Producer, 100 | )) 101 | v1Router.Get("/expressions", handlers.HandlerGetExpressions(log, dbCfg, cfg.JWTSecret)) 102 | 103 | // Operation endpoints 104 | v1Router.Get("/operations", handlers.HandlerGetOperations(log, dbCfg, cfg.JWTSecret)) 105 | v1Router.Patch("/operations", handlers.HandlerUpdateOperation(log, dbCfg, cfg.JWTSecret)) 106 | 107 | // Agent endpoints 108 | v1Router.Get("/agents", handlers.HandlerGetAgents(log, dbCfg)) 109 | 110 | // User endpoints 111 | v1Router.Post("/login", handlers.HandlerLoginUser(log, dbCfg, grpcClient)) 112 | v1Router.Post("/register", handlers.HandlerRegisterNewUser(log, dbCfg, grpcClient)) 113 | 114 | router.Mount("/v1", v1Router) 115 | 116 | srv := &http.Server{ 117 | Handler: router, 118 | Addr: cfg.HTTPServer.Address, 119 | ReadTimeout: cfg.Timeout, 120 | WriteTimeout: cfg.Timeout, 121 | IdleTimeout: cfg.IdleTimeout, 122 | } 123 | 124 | log.Info("server starting", slog.String("host", cfg.HTTPServer.Address)) 125 | 126 | go func() { 127 | if err = srv.ListenAndServe(); err != nil { 128 | log.Error("failed to start server ", sl.Err(err)) 129 | } 130 | }() 131 | 132 | log.Info("http-server stopped") 133 | 134 | // Graceful shotdown 135 | stop := make(chan os.Signal, 1) 136 | signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) 137 | 138 | sign := <-stop 139 | 140 | log.Info("stopping http-server", slog.String("signal", sign.String())) 141 | 142 | application.Stop(ctxWithCancel, cfg) 143 | 144 | log.Info("http-server stopped") 145 | } 146 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/parser/infix_to_postfix_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator/parser" 9 | ) 10 | 11 | func TestInfixToPostfix(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | expression string 15 | wantedExpression string 16 | err error 17 | }{ 18 | { 19 | name: "Basic arithmetic expressions - '+'", 20 | expression: "3+4", 21 | wantedExpression: "3 4 +", 22 | err: nil, 23 | }, 24 | { 25 | name: "Basic arithmetic expressions - '-'", 26 | expression: "3-4", 27 | wantedExpression: "3 4 -", 28 | err: nil, 29 | }, 30 | { 31 | name: "Basic arithmetic expressions - '*'", 32 | expression: "3*4", 33 | wantedExpression: "3 4 *", 34 | err: nil, 35 | }, 36 | { 37 | name: "Basic arithmetic expressions - '/'", 38 | expression: "3/4", 39 | wantedExpression: "3 4 /", 40 | err: nil, 41 | }, 42 | { 43 | name: "Expression with parentheses at the beginning with plus operator", 44 | expression: "(3+4)*5", 45 | wantedExpression: "3 4 + 5 *", 46 | err: nil, 47 | }, 48 | { 49 | name: "Expression with parentheses at the beginning with product operator", 50 | expression: "(3*4)+5", 51 | wantedExpression: "3 4 * 5 +", 52 | err: nil, 53 | }, 54 | { 55 | name: "Expression with parentheses at the end with plus operator", 56 | expression: "3*(4+5)", 57 | wantedExpression: "3 4 5 + *", 58 | err: nil, 59 | }, 60 | { 61 | name: "Expression with 2 parentheses at the end and at the beginning", 62 | expression: "(3+4)*(5-6)", 63 | wantedExpression: "3 4 + 5 6 - *", 64 | err: nil, 65 | }, 66 | { 67 | name: "Complex expressions 1", 68 | expression: "3+4*5", 69 | wantedExpression: "3 4 5 * +", 70 | err: nil, 71 | }, 72 | { 73 | name: "Complex expressions 2", 74 | expression: "3*4+5", 75 | wantedExpression: "3 4 * 5 +", 76 | err: nil, 77 | }, 78 | { 79 | name: "Complex expressions 3", 80 | expression: "(3+4)*5-6/2", 81 | wantedExpression: "3 4 + 5 * 6 2 / -", 82 | err: nil, 83 | }, 84 | { 85 | name: "Complex expressions 4", 86 | expression: "3*(4+5)-6/(2+1)", 87 | wantedExpression: "3 4 5 + * 6 2 1 + / -", 88 | err: nil, 89 | }, 90 | { 91 | name: "Expression with multiple operators of the same precedence 1", 92 | expression: "3+4-5", 93 | wantedExpression: "3 4 + 5 -", 94 | err: nil, 95 | }, 96 | { 97 | name: "Expression with multiple operators of the same precedence 2", 98 | expression: "3*4/5", 99 | wantedExpression: "3 4 * 5 /", 100 | err: nil, 101 | }, 102 | { 103 | name: "Expression with multiple operators of the same precedence 3", 104 | expression: "(3+4)-(5+6)", 105 | wantedExpression: "3 4 + 5 6 + -", 106 | err: nil, 107 | }, 108 | { 109 | name: "Expression with multiple operators of the same precedence 4", 110 | expression: "(3*4)/(5*6)", 111 | wantedExpression: "3 4 * 5 6 * /", 112 | err: nil, 113 | }, 114 | { 115 | name: "Invalid expression", 116 | expression: "5+)+3", 117 | wantedExpression: "", 118 | err: errors.New("invalid expression"), 119 | }, 120 | } 121 | 122 | for _, tc := range testCases { 123 | tc := tc 124 | t.Run(tc.name, func(t *testing.T) { 125 | got, err := parser.InfixToPostfix(tc.expression) 126 | if got != tc.wantedExpression { 127 | t.Errorf( 128 | "InfixToPostfix(%v) = %v, %v; want %v, but got %v", 129 | tc.expression, got, err, 130 | tc.wantedExpression, got, 131 | ) 132 | } 133 | if tc.err != nil && (err == nil || !strings.Contains(err.Error(), tc.err.Error())) { 134 | t.Errorf( 135 | "InfixToPostfix(%v) = %v, %v; expected error containing '%v', but got %v", 136 | tc.expression, got, err, 137 | tc.err, err, 138 | ) 139 | } else if tc.err == nil && err != nil { 140 | t.Errorf( 141 | "InfixToPostfix(%v) = %v, %v; expected no error, but got %v", 142 | tc.expression, got, err, 143 | err, 144 | ) 145 | } 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DAEC-fullstack 2 | 3 | ![golanci lint](https://github.com/Prrromanssss/DAEC-fullstack/actions/workflows/golangci-lint.yml/badge.svg) 4 | ![golanci test](https://github.com/Prrromanssss/DAEC-fullstack/actions/workflows/golangci-test.yml/badge.svg) 5 | 6 | ## About 7 | 8 | **This is distributed arithmetic expression calculator.** 9 | 10 | ![Main page](https://github.com/Prrromanssss/DAEC-fullstack/raw/main/images/expressions.png) 11 | ![Agents](https://github.com/Prrromanssss/DAEC-fullstack/raw/main/images/agents.png) 12 | In the photo, we can see two agents working (one gorutine from each) and one agent died. 13 | 14 | ### Description 15 | 16 | The user wants to calculate arithmetic expressions. He enters the code 2 + 2 * 2 and wants to get the answer 6. But our addition and multiplication (also subtraction) operations take a “very, very” long time. Therefore, the option in which the user makes an http request and receives the result as a response is impossible. 17 | Moreover: the calculation of each such operation in our “alternative reality” takes “giant” computing power. Accordingly, we must be able to perform each action separately and we can scale this system by adding computing power to our system in the form of new “machines”. 18 | Therefore, when a user sends an expression, he receives an expression identifier in response and can, at some periodicity, check with the server whether the expression has been counted? If the expression is finally evaluated, he will get the result. Remember that some parts of an arithmetic expression can be evaluated in parallel. 19 | 20 | 21 | ### How to use it? 22 | 23 | Expressions - You can write some expressions to calculate (registered users only). 24 | 25 | Operations - You can change the execution time of each operation (registered users only). 26 | 27 | Agents - You can see how many servers can currently process expressions. 28 | 29 | Login - You can register or log in to your account. 30 | 31 | ### How does it work? 32 | 33 | *Orchestrator*: 34 | 1. HTTP-server 35 | 2. Parses expression from users and sends to Agents through RabbitMQ. 36 | 3. Consumes results from Agents, inserts them to the expressions and sends new tokens to Agents to calculate. 37 | 4. Consumes pings from Agents and kills those who didn't send anything. 38 | 5. Writing the data to the database. 39 | 40 | *Agent*: 41 | 1. Consumes expressions from the Orchestartor and gives it to its goroutines for calculations. 42 | 2. Consumes results from each goroutine and sends it to Orchestartor through RabbitMQ. 43 | 3. Sends pings to Orchestartor. 44 | 4. Every agent have 5 goroutines. 45 | 5. There are 3 agents. 46 | 47 | *Auth*: 48 | 1. Log in to user's account. 49 | 2. Register new users. 50 | 51 | ### What about parallelism? 52 | 53 | Some example: 54 | 55 | I uses reverse Polish notation 56 | 57 | 2 + 2 --parse--> 2 2 + 58 | 59 | And we can give 2 2 + to some goroutine to calculate. 60 | 61 | But what about this example? 62 | 63 | 2 + 2 + 2 + 2 --parse--> 2 2 + 2 + 2 + 64 | 65 | I think it's slow, because we need to solve 2 2 +, then 4 2 +, then 6 2 + 66 | 67 | SO, I parses it to RPN differently. 68 | 69 | I just add some brackets to expression. 70 | 71 | 2 + 2 + 2 + 2 --add-brackets--> (2 + 2) + (2 + 2) --parse--> 2 2 + 2 2 + + 72 | 73 | And now we can run parallel 2 2 + and 2 2 + and then just add up their results. 74 | 75 | We have N expressions, every expression is processed by some agent. 76 | But that's not all, inside each expression we process subexpressions with different agents. 77 | 78 | If the HTTP-server crashed and we have expressions that did not have time to be calculated, by rebooting the server we will return to their calculations. 79 | 80 | ## Deployment instructions 81 | 82 | ### 1. Cloning project from GitHub 83 | 84 | Run this command 85 | ```commandline 86 | git clone https://github.com/Prrromanssss/DAEC-fullstack 87 | ``` 88 | 89 | ### 2. Build and run application 90 | Run this command 91 | ```comandline 92 | docker-compose up -d 93 | ``` 94 | 95 | ### 3. Follow link 96 | ```commandline 97 | http://127.0.0.1:5173/ 98 | ``` 99 | 100 | ## Testing 101 | I have unit-tests to test the work of my parser. 102 | You can see that all tests have passed in github actions. 103 | 104 | ### Some expressions to test calculator 105 | - Valid cases 106 | 1. 4 + -2 + 5 * 6 107 | 2. 2 + 2 + 2 + 2 108 | 3. 2 + 2 * 4 + 3 - 4 + 5 109 | 4. (23 + 125) - 567 * 23 110 | 5. -3 +6 111 | - Invalid cases 112 | 1. 4 / 0 113 | 2. 45 + x - 5 114 | 3. 45 + 4* 115 | 4. ---4 + 5 116 | 5. 52 * 3 / 117 | 118 | ## Schema 119 | ![Schema of the project](https://github.com/Prrromanssss/DAEC-fullstack/raw/main/images/schema.png) 120 | 121 | ## ER-diagram 122 | ![ER-diagram of the project](https://github.com/Prrromanssss/DAEC-fullstack/raw/main/images/ERD.png) 123 | -------------------------------------------------------------------------------- /backend/internal/protos/gen/go/daec/daec_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v4.25.3 5 | // source: daec/daec.proto 6 | 7 | package daecv1 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // AuthClient is the client API for Auth service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type AuthClient interface { 25 | Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) 26 | Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) 27 | } 28 | 29 | type authClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { 34 | return &authClient{cc} 35 | } 36 | 37 | func (c *authClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) { 38 | out := new(RegisterResponse) 39 | err := c.cc.Invoke(ctx, "/auth.Auth/Register", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { 47 | out := new(LoginResponse) 48 | err := c.cc.Invoke(ctx, "/auth.Auth/Login", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | // AuthServer is the server API for Auth service. 56 | // All implementations must embed UnimplementedAuthServer 57 | // for forward compatibility 58 | type AuthServer interface { 59 | Register(context.Context, *RegisterRequest) (*RegisterResponse, error) 60 | Login(context.Context, *LoginRequest) (*LoginResponse, error) 61 | mustEmbedUnimplementedAuthServer() 62 | } 63 | 64 | // UnimplementedAuthServer must be embedded to have forward compatible implementations. 65 | type UnimplementedAuthServer struct { 66 | } 67 | 68 | func (UnimplementedAuthServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { 69 | return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") 70 | } 71 | func (UnimplementedAuthServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { 72 | return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") 73 | } 74 | func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {} 75 | 76 | // UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. 77 | // Use of this interface is not recommended, as added methods to AuthServer will 78 | // result in compilation errors. 79 | type UnsafeAuthServer interface { 80 | mustEmbedUnimplementedAuthServer() 81 | } 82 | 83 | func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) { 84 | s.RegisterService(&Auth_ServiceDesc, srv) 85 | } 86 | 87 | func _Auth_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 88 | in := new(RegisterRequest) 89 | if err := dec(in); err != nil { 90 | return nil, err 91 | } 92 | if interceptor == nil { 93 | return srv.(AuthServer).Register(ctx, in) 94 | } 95 | info := &grpc.UnaryServerInfo{ 96 | Server: srv, 97 | FullMethod: "/auth.Auth/Register", 98 | } 99 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 100 | return srv.(AuthServer).Register(ctx, req.(*RegisterRequest)) 101 | } 102 | return interceptor(ctx, in, info, handler) 103 | } 104 | 105 | func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(LoginRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(AuthServer).Login(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: "/auth.Auth/Login", 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(AuthServer).Login(ctx, req.(*LoginRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | // Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. 124 | // It's only intended for direct use with grpc.RegisterService, 125 | // and not to be introspected or modified (even as a copy) 126 | var Auth_ServiceDesc = grpc.ServiceDesc{ 127 | ServiceName: "auth.Auth", 128 | HandlerType: (*AuthServer)(nil), 129 | Methods: []grpc.MethodDesc{ 130 | { 131 | MethodName: "Register", 132 | Handler: _Auth_Register_Handler, 133 | }, 134 | { 135 | MethodName: "Login", 136 | Handler: _Auth_Login_Handler, 137 | }, 138 | }, 139 | Streams: []grpc.StreamDesc{}, 140 | Metadata: "daec/daec.proto", 141 | } 142 | -------------------------------------------------------------------------------- /backend/internal/app/orchestrator/app.go: -------------------------------------------------------------------------------- 1 | package orchestratorapp 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/config" 9 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/brokers" 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 11 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 12 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/pool" 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 14 | "github.com/streadway/amqp" 15 | 16 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator" 17 | "github.com/Prrromanssss/DAEC-fullstack/internal/rabbitmq" 18 | ) 19 | 20 | type App struct { 21 | log *slog.Logger 22 | OrchestratorApp *orchestrator.Orchestrator 23 | workerPool *pool.MyPool 24 | amqpConfig rabbitmq.AMQPConfig 25 | channelForProducer *amqp.Channel 26 | channelForConsumer *amqp.Channel 27 | Producer brokers.Producer 28 | Consumer brokers.Consumer 29 | } 30 | 31 | // MustRun runs Orchestrator and panics if any error occurs. 32 | func (a *App) MustRun(ctx context.Context) { 33 | if err := a.Run(ctx); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | // New creates new Orchestrator app. 39 | func New( 40 | log *slog.Logger, 41 | cfg *config.Config, 42 | dbCfg *storage.Storage, 43 | cancel context.CancelFunc, 44 | ) (*App, error) { 45 | amqpCfg, err := rabbitmq.NewAMQPConfig(log, cfg.RabbitMQURL) 46 | if err != nil { 47 | log.Error("can't create NewAMQPConfig", sl.Err(err)) 48 | return nil, err 49 | } 50 | 51 | producer, err := rabbitmq.NewAMQPProducer(log, amqpCfg, cfg.QueueForExpressionsToAgents) 52 | if err != nil { 53 | log.Error("can't create NewAMQPProducer", sl.Err(err)) 54 | return nil, err 55 | } 56 | 57 | consumer, err := rabbitmq.NewAMQPConsumer(log, amqpCfg, cfg.QueueForResultsFromAgents) 58 | if err != nil { 59 | log.Error("can't create NewAMQPConsumer", sl.Err(err)) 60 | return nil, err 61 | } 62 | 63 | orc, err := orchestrator.NewOrchestrator( 64 | log, 65 | dbCfg, 66 | cfg.InactiveTimeForAgent, 67 | cancel, 68 | ) 69 | if err != nil { 70 | log.Error("orchestrator error", sl.Err(err)) 71 | return nil, err 72 | } 73 | // Create worker pool with 5 workers. 74 | workerPool, err := pool.NewWorkerPool(5, 10) 75 | if err != nil { 76 | log.Error("can't create worker pool", sl.Err(err)) 77 | } 78 | 79 | return &App{ 80 | log: log, 81 | OrchestratorApp: orc, 82 | workerPool: workerPool, 83 | amqpConfig: *amqpCfg, 84 | channelForProducer: producer.Channel, 85 | channelForConsumer: producer.Channel, 86 | Producer: producer, 87 | Consumer: consumer, 88 | }, nil 89 | } 90 | 91 | // RunOrchestrator agregates agents, 92 | // consumes messages from client, manages their job. 93 | func (a *App) Run(ctx context.Context) error { 94 | defer func() { 95 | a.amqpConfig.Close() 96 | a.Producer.Close() 97 | a.Consumer.Close() 98 | a.workerPool.Stop() 99 | }() 100 | 101 | const fn = "orchestratorapp.Run" 102 | 103 | log := a.log.With( 104 | slog.String("fn", fn), 105 | ) 106 | 107 | a.workerPool.Start() 108 | 109 | // Reload not completed expressions. 110 | err := a.OrchestratorApp.ReloadComputingExpressions(ctx, a.Producer) 111 | if err != nil { 112 | log.Error("can't reload computing expressions", sl.Err(err)) 113 | 114 | return err 115 | } 116 | 117 | ticker := time.NewTicker(time.Duration(a.OrchestratorApp.InactiveTimeForAgent) * time.Second) 118 | defer ticker.Stop() 119 | 120 | for { 121 | select { 122 | // TODO: Need to syncronize goroutines 123 | case msgFromAgents := <-a.Consumer.GetMessages(): 124 | task := orchestrator.ExecuteWrapper(a.OrchestratorApp, ctx, msgFromAgents, a.Producer) 125 | a.workerPool.AddWork(task) 126 | time.Sleep(time.Second) 127 | case <-ticker.C: 128 | err := a.OrchestratorApp.CheckPing(ctx, a.Producer) 129 | if err != nil { 130 | log.Warn("can't check pings from agents", sl.Err(err)) 131 | } 132 | 133 | err = a.OrchestratorApp.FindForgottenExpressions(ctx, a.Producer) 134 | if err != nil { 135 | log.Warn("can't find forgotten expressions", sl.Err(err)) 136 | } 137 | case <-ctx.Done(): 138 | log.Error("orchestrator stopped") 139 | 140 | return ctx.Err() 141 | } 142 | } 143 | } 144 | 145 | // // Stop stops Orchestrator app. 146 | func (a *App) Stop(ctx context.Context, cfg *config.Config) { 147 | if _, err := a.channelForConsumer.QueuePurge(cfg.QueueForResultsFromAgents, false); err != nil { 148 | a.log.Error("can't purged queue", slog.String("queue", cfg.QueueForResultsFromAgents)) 149 | } 150 | if _, err := a.channelForProducer.QueuePurge(cfg.QueueForExpressionsToAgents, false); err != nil { 151 | a.log.Error("can't purged queue", slog.String("queue", cfg.QueueForExpressionsToAgents)) 152 | } 153 | if err := a.Producer.PublishExpressionMessage(&messages.ExpressionMessage{ 154 | Kill: true, 155 | }); err != nil { 156 | a.log.Error("can't send kill message to agent") 157 | } 158 | if err := a.Producer.PublishExpressionMessage(&messages.ExpressionMessage{ 159 | Kill: true, 160 | }); err != nil { 161 | a.log.Error("can't send kill message to agent") 162 | } 163 | if err := a.Producer.PublishExpressionMessage(&messages.ExpressionMessage{ 164 | Kill: true, 165 | }); err != nil { 166 | a.log.Error("can't send kill message to agent") 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | // ParseExpression parses the expression from the user. 11 | func ParseExpression(expression string) (string, error) { 12 | rawExpression := strings.ReplaceAll(expression, " ", "") 13 | if !IsValidExpression(rawExpression) { 14 | return "", errors.New("invalid expression") 15 | } 16 | rawExpression = AddBrackets(rawExpression) 17 | result, err := InfixToPostfix(rawExpression) 18 | if err != nil { 19 | return "", err 20 | } 21 | return result, nil 22 | } 23 | 24 | // IsValidExpression checks whether the eexpression is valid or not. 25 | func IsValidExpression(expression string) bool { 26 | stack := make([]rune, 0) 27 | 28 | if expression == "" { 29 | return false 30 | } 31 | 32 | for i, char := range expression { 33 | switch char { 34 | case '(': 35 | stack = append(stack, char) 36 | case ')': 37 | if len(stack) == 0 || stack[len(stack)-1] != '(' || (i > 0 && expression[i-1] == '(') { 38 | return false 39 | } 40 | stack = stack[:len(stack)-1] 41 | case '*', '/': 42 | if i == 0 || i == len(expression)-1 { 43 | return false 44 | } 45 | if contains([]rune{'+', '-', '*', '/', '(', ' '}, rune(expression[i-1])) || 46 | contains([]rune{'+', '-', '*', '/', ')'}, rune(expression[i+1])) { 47 | return false 48 | } 49 | if i+1 < len(expression) && expression[i+1] == '0' { 50 | return false 51 | } 52 | case '-', '+': 53 | if i == len(expression)-1 { 54 | return false 55 | } 56 | 57 | if i == 0 || i == 1 || expression[i-1] == '(' { 58 | if expression[i+1] == ')' { 59 | return false 60 | } 61 | continue 62 | } 63 | 64 | if contains([]rune{'+', '-', '*', '/', ' '}, rune(expression[i-1])) && 65 | contains([]rune{'+', '-', '*', '/', '(', ' '}, rune(expression[i-2])) { 66 | return false 67 | } 68 | default: 69 | if !unicode.IsDigit(char) { 70 | return false 71 | } 72 | if i > 0 && expression[i-1] == '0' { 73 | return false 74 | } 75 | } 76 | } 77 | 78 | return len(stack) == 0 79 | } 80 | 81 | // AddBrackets adds brackets to espression in order to parallelize some operations. 82 | func AddBrackets(expression string) string { 83 | var result string 84 | 85 | parts := strings.FieldsFunc(addZeroToUnaryPlusAndMinus(expression), func(r rune) bool { 86 | return r == '+' || r == '-' 87 | }) 88 | length := len(parts) 89 | sliceOfOrdersPlusMinus := orderPlusMinus(addZeroToUnaryPlusAndMinus(expression)) 90 | var ind, indForOrdersPlusMinus int 91 | if len(parts) <= 2 { 92 | return expression 93 | } 94 | for ind < length { 95 | currentOperator := string(sliceOfOrdersPlusMinus[indForOrdersPlusMinus]) 96 | currentSymbol := parts[ind] 97 | 98 | if ind == 0 && 99 | IsNumber(currentSymbol) && 100 | IsNumber(parts[ind+1]) && 101 | sliceOfOrdersPlusMinus[indForOrdersPlusMinus+1] == '+' { 102 | 103 | result += "(" + currentSymbol + currentOperator + parts[ind+1] + ")" 104 | indForOrdersPlusMinus++ 105 | ind++ 106 | } else if ind == 0 && 107 | ((IsNumber(currentSymbol) && !IsNumber(parts[ind+1])) || 108 | !IsNumber(currentSymbol)) { 109 | 110 | result += currentSymbol 111 | } else if ind == 0 { 112 | result += currentSymbol 113 | } else if ind+1 < length && 114 | IsNumber(currentSymbol) && 115 | IsNumber(parts[ind+1]) && 116 | currentOperator == "+" && 117 | (indForOrdersPlusMinus+2 >= len(sliceOfOrdersPlusMinus) || 118 | sliceOfOrdersPlusMinus[indForOrdersPlusMinus+2] == '+') { 119 | 120 | result += currentOperator + "(" + currentSymbol 121 | indForOrdersPlusMinus++ 122 | result += string(sliceOfOrdersPlusMinus[indForOrdersPlusMinus]) + parts[ind+1] + ")" 123 | indForOrdersPlusMinus++ 124 | ind++ 125 | } else { 126 | result += currentOperator + currentSymbol 127 | indForOrdersPlusMinus++ 128 | } 129 | ind++ 130 | } 131 | result = strings.ReplaceAll(result, "&", "+") 132 | result = strings.ReplaceAll(result, "$", "-") 133 | 134 | return result 135 | } 136 | 137 | func addZeroToUnaryPlusAndMinus(expression string) string { 138 | var result strings.Builder 139 | length := len(expression) 140 | ind := 0 141 | for ind < length { 142 | if ind+1 < length && contains([]rune{'+', '-', '*', '/'}, rune(expression[ind])) && expression[ind+1] == '+' { 143 | result.WriteRune(rune(expression[ind])) 144 | result.WriteRune('0') 145 | result.WriteRune('+') 146 | ind++ 147 | } else if ind == 0 && expression[ind] == '+' { 148 | result.WriteRune('0') 149 | result.WriteRune('+') 150 | } else if ind+1 < length && contains([]rune{'+', '-', '*', '/'}, rune(expression[ind])) && expression[ind+1] == '-' { 151 | result.WriteRune(rune(expression[ind])) 152 | result.WriteRune('0') 153 | result.WriteRune('-') 154 | ind++ 155 | } else if ind == 0 && expression[ind] == '-' { 156 | result.WriteRune('0') 157 | result.WriteRune('-') 158 | } else { 159 | result.WriteRune(rune(expression[ind])) 160 | } 161 | ind++ 162 | // log.Println(result.String()) 163 | } 164 | return result.String() 165 | } 166 | 167 | func orderPlusMinus(expression string) []rune { 168 | res := make([]rune, 0) 169 | for _, char := range expression { 170 | if char == '-' || char == '+' { 171 | res = append(res, char) 172 | } 173 | } 174 | return res 175 | } 176 | 177 | // IsNumber checks if s is a number. 178 | func IsNumber(s string) bool { 179 | _, err := strconv.ParseFloat(s, 64) 180 | return err == nil 181 | } 182 | 183 | func contains(arr []rune, element rune) bool { 184 | for _, elem := range arr { 185 | if elem == element { 186 | return true 187 | } 188 | } 189 | return false 190 | } 191 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/parser/tokens_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 9 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/handlers/slogdiscard" 10 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator/parser" 11 | ) 12 | 13 | func TestGetTokens(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | expression string 17 | wantedTokens []string 18 | }{ 19 | { 20 | name: "one token, only plus operator", 21 | expression: "1 1 +", 22 | wantedTokens: []string{"1 1 +"}, 23 | }, 24 | { 25 | name: "one token, only minus operator", 26 | expression: "1 1 -", 27 | wantedTokens: []string{"1 1 -"}, 28 | }, 29 | { 30 | name: "one token, only product operator", 31 | expression: "1 1 *", 32 | wantedTokens: []string{"1 1 *"}, 33 | }, 34 | { 35 | name: "one token, only division operator", 36 | expression: "1 1 /", 37 | wantedTokens: []string{"1 1 /"}, 38 | }, 39 | { 40 | name: "three tokens, only plus operator", 41 | expression: "1 1 + 2 2 + + 3 3 + +", 42 | wantedTokens: []string{"1 1 +", "2 2 +", "3 3 +"}, 43 | }, 44 | { 45 | name: "three tokens, big numbers, different operators", 46 | expression: "1345 1123 + 9 223 - + 9 3 * +", 47 | wantedTokens: []string{"1345 1123 +", "9 223 -", "9 3 *"}, 48 | }, 49 | } 50 | 51 | log := slogdiscard.NewDiscardLogger() 52 | 53 | for _, tc := range testCases { 54 | tc := tc 55 | t.Run(tc.name, func(t *testing.T) { 56 | got := parser.GetTokens(log, tc.expression) 57 | if !slicesEqual(got, tc.wantedTokens) { 58 | t.Errorf("GetTokens(log, %v) = %v; want %v", tc.expression, got, tc.wantedTokens) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestInsertResultToToken(t *testing.T) { 65 | testCases := []struct { 66 | name string 67 | expression string 68 | token string 69 | result int 70 | err error 71 | wantedExpression messages.ResultAndTokenMessage 72 | }{ 73 | { 74 | name: "Correct case with token at the beginning of the expression", 75 | expression: "3 3 + 4 + 122 +", 76 | token: "3 3 +", 77 | result: 6, 78 | err: nil, 79 | wantedExpression: messages.ResultAndTokenMessage{ 80 | Result: "6 4 + 122 +", 81 | Token: "6 4 +", 82 | }, 83 | }, 84 | { 85 | name: "Correct case with token at the end of the expression", 86 | expression: "3 3 + 4 + 55 67 + +", 87 | token: "55 67 +", 88 | result: 122, 89 | err: nil, 90 | wantedExpression: messages.ResultAndTokenMessage{ 91 | Result: "3 3 + 4 + 122 +", 92 | Token: "", 93 | }, 94 | }, 95 | { 96 | name: "Correct case with token in the middle of the expression", 97 | expression: "1 1 + 2 2 + + 3 3 + +", 98 | token: "2 2 +", 99 | result: 4, 100 | err: nil, 101 | wantedExpression: messages.ResultAndTokenMessage{ 102 | Result: "1 1 + 4 + 3 3 + +", 103 | Token: "", 104 | }, 105 | }, 106 | { 107 | name: "Correct case with a token length of a three", 108 | expression: "45 23 +", 109 | token: "45 23 +", 110 | result: 68, 111 | err: nil, 112 | wantedExpression: messages.ResultAndTokenMessage{ 113 | Result: "68", 114 | Token: "", 115 | }, 116 | }, 117 | { 118 | name: "Incorrect case with missing token", 119 | expression: "3 3 + 4 + 55 67 + +", 120 | token: "155 67 +", 121 | result: 222, 122 | err: errors.New("can't find token"), 123 | wantedExpression: messages.ResultAndTokenMessage{ 124 | Result: "", 125 | Token: "", 126 | }, 127 | }, 128 | { 129 | name: "Incorrect case with invalid expression", 130 | expression: "3 3 + 4 + 55 67 +", 131 | token: "55 67 +", 132 | result: 122, 133 | err: errors.New("invalid expression"), 134 | wantedExpression: messages.ResultAndTokenMessage{ 135 | Result: "", 136 | Token: "", 137 | }, 138 | }, 139 | } 140 | 141 | for _, tc := range testCases { 142 | tc := tc 143 | t.Run(tc.name, func(t *testing.T) { 144 | got, err := parser.InsertResultToToken(tc.expression, tc.token, tc.result) 145 | if got.Result != tc.wantedExpression.Result { 146 | t.Errorf( 147 | "InsertResultToToken(%v, %v, %v) = %v, %v; want %v, but got %v", 148 | tc.expression, tc.token, tc.result, 149 | got, err, 150 | tc.wantedExpression.Result, got.Result, 151 | ) 152 | } 153 | if got.Token != tc.wantedExpression.Token { 154 | t.Errorf( 155 | "InsertResultToToken(%v, %v, %v) = %v, %v; want %v, but got %v", 156 | tc.expression, tc.token, tc.result, 157 | got, err, 158 | tc.wantedExpression.Token, got.Token, 159 | ) 160 | } 161 | if tc.err != nil && (err == nil || !strings.Contains(err.Error(), tc.err.Error())) { 162 | t.Errorf( 163 | "InsertResultToToken(%v, %v, %v) = %v, %v; expected error containing '%v', but got %v", 164 | tc.expression, tc.token, tc.result, 165 | got, err, 166 | tc.err, err, 167 | ) 168 | } else if tc.err == nil && err != nil { 169 | t.Errorf( 170 | "InsertResultToToken(%v, %v, %v) = %v, %v; expected no error, but got %v", 171 | tc.expression, tc.token, tc.result, 172 | got, err, 173 | err, 174 | ) 175 | } 176 | }) 177 | } 178 | } 179 | 180 | func slicesEqual(slice1, slice2 []string) bool { 181 | if len(slice1) != len(slice2) { 182 | return false 183 | } 184 | 185 | for i := 0; i < len(slice1); i++ { 186 | if slice1[i] != slice2[i] { 187 | return false 188 | } 189 | } 190 | 191 | return true 192 | } 193 | -------------------------------------------------------------------------------- /backend/internal/storage/postgres/agents.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | // source: agents.sql 5 | 6 | package postgres 7 | 8 | import ( 9 | "context" 10 | "time" 11 | ) 12 | 13 | const createAgent = `-- name: CreateAgent :one 14 | INSERT INTO agents 15 | (created_at, number_of_parallel_calculations, last_ping, status) 16 | VALUES 17 | ($1, $2, $3, $4) 18 | RETURNING 19 | agent_id, number_of_parallel_calculations, 20 | last_ping, status, created_at, 21 | number_of_active_calculations 22 | ` 23 | 24 | type CreateAgentParams struct { 25 | CreatedAt time.Time 26 | NumberOfParallelCalculations int32 27 | LastPing time.Time 28 | Status AgentStatus 29 | } 30 | 31 | func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent, error) { 32 | row := q.db.QueryRowContext(ctx, createAgent, 33 | arg.CreatedAt, 34 | arg.NumberOfParallelCalculations, 35 | arg.LastPing, 36 | arg.Status, 37 | ) 38 | var i Agent 39 | err := row.Scan( 40 | &i.AgentID, 41 | &i.NumberOfParallelCalculations, 42 | &i.LastPing, 43 | &i.Status, 44 | &i.CreatedAt, 45 | &i.NumberOfActiveCalculations, 46 | ) 47 | return i, err 48 | } 49 | 50 | const decrementNumberOfActiveCalculations = `-- name: DecrementNumberOfActiveCalculations :exec 51 | UPDATE agents 52 | SET number_of_active_calculations = number_of_active_calculations - 1 53 | WHERE agent_id = $1 54 | ` 55 | 56 | func (q *Queries) DecrementNumberOfActiveCalculations(ctx context.Context, agentID int32) error { 57 | _, err := q.db.ExecContext(ctx, decrementNumberOfActiveCalculations, agentID) 58 | return err 59 | } 60 | 61 | const getAgents = `-- name: GetAgents :many 62 | SELECT 63 | agent_id, number_of_parallel_calculations, 64 | last_ping, status, created_at, 65 | number_of_active_calculations 66 | FROM agents 67 | ORDER BY created_at DESC 68 | ` 69 | 70 | func (q *Queries) GetAgents(ctx context.Context) ([]Agent, error) { 71 | rows, err := q.db.QueryContext(ctx, getAgents) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer rows.Close() 76 | var items []Agent 77 | for rows.Next() { 78 | var i Agent 79 | if err := rows.Scan( 80 | &i.AgentID, 81 | &i.NumberOfParallelCalculations, 82 | &i.LastPing, 83 | &i.Status, 84 | &i.CreatedAt, 85 | &i.NumberOfActiveCalculations, 86 | ); err != nil { 87 | return nil, err 88 | } 89 | items = append(items, i) 90 | } 91 | if err := rows.Close(); err != nil { 92 | return nil, err 93 | } 94 | if err := rows.Err(); err != nil { 95 | return nil, err 96 | } 97 | return items, nil 98 | } 99 | 100 | const getBusyAgents = `-- name: GetBusyAgents :many 101 | SELECT agent_id 102 | FROM agents 103 | WHERE number_of_active_calculations != 0 104 | ` 105 | 106 | func (q *Queries) GetBusyAgents(ctx context.Context) ([]int32, error) { 107 | rows, err := q.db.QueryContext(ctx, getBusyAgents) 108 | if err != nil { 109 | return nil, err 110 | } 111 | defer rows.Close() 112 | var items []int32 113 | for rows.Next() { 114 | var agent_id int32 115 | if err := rows.Scan(&agent_id); err != nil { 116 | return nil, err 117 | } 118 | items = append(items, agent_id) 119 | } 120 | if err := rows.Close(); err != nil { 121 | return nil, err 122 | } 123 | if err := rows.Err(); err != nil { 124 | return nil, err 125 | } 126 | return items, nil 127 | } 128 | 129 | const incrementNumberOfActiveCalculations = `-- name: IncrementNumberOfActiveCalculations :exec 130 | UPDATE agents 131 | SET number_of_active_calculations = number_of_active_calculations + 1 132 | WHERE agent_id = $1 133 | ` 134 | 135 | func (q *Queries) IncrementNumberOfActiveCalculations(ctx context.Context, agentID int32) error { 136 | _, err := q.db.ExecContext(ctx, incrementNumberOfActiveCalculations, agentID) 137 | return err 138 | } 139 | 140 | const terminateAgents = `-- name: TerminateAgents :many 141 | UPDATE agents 142 | SET status = 'terminated', number_of_active_calculations = 0 143 | WHERE EXTRACT(SECOND FROM NOW()::timestamp - agents.last_ping) > $1::numeric 144 | RETURNING agent_id 145 | ` 146 | 147 | func (q *Queries) TerminateAgents(ctx context.Context, dollar_1 string) ([]int32, error) { 148 | rows, err := q.db.QueryContext(ctx, terminateAgents, dollar_1) 149 | if err != nil { 150 | return nil, err 151 | } 152 | defer rows.Close() 153 | var items []int32 154 | for rows.Next() { 155 | var agent_id int32 156 | if err := rows.Scan(&agent_id); err != nil { 157 | return nil, err 158 | } 159 | items = append(items, agent_id) 160 | } 161 | if err := rows.Close(); err != nil { 162 | return nil, err 163 | } 164 | if err := rows.Err(); err != nil { 165 | return nil, err 166 | } 167 | return items, nil 168 | } 169 | 170 | const terminateOldAgents = `-- name: TerminateOldAgents :exec 171 | DELETE FROM agents 172 | WHERE status = 'terminated' 173 | ` 174 | 175 | func (q *Queries) TerminateOldAgents(ctx context.Context) error { 176 | _, err := q.db.ExecContext(ctx, terminateOldAgents) 177 | return err 178 | } 179 | 180 | const updateAgentLastPing = `-- name: UpdateAgentLastPing :exec 181 | UPDATE agents 182 | SET last_ping = $1 183 | WHERE agent_id = $2 184 | ` 185 | 186 | type UpdateAgentLastPingParams struct { 187 | LastPing time.Time 188 | AgentID int32 189 | } 190 | 191 | func (q *Queries) UpdateAgentLastPing(ctx context.Context, arg UpdateAgentLastPingParams) error { 192 | _, err := q.db.ExecContext(ctx, updateAgentLastPing, arg.LastPing, arg.AgentID) 193 | return err 194 | } 195 | 196 | const updateAgentStatus = `-- name: UpdateAgentStatus :exec 197 | UPDATE agents 198 | SET status = $1 199 | WHERE agent_id = $2 200 | ` 201 | 202 | type UpdateAgentStatusParams struct { 203 | Status AgentStatus 204 | AgentID int32 205 | } 206 | 207 | func (q *Queries) UpdateAgentStatus(ctx context.Context, arg UpdateAgentStatusParams) error { 208 | _, err := q.db.ExecContext(ctx, updateAgentStatus, arg.Status, arg.AgentID) 209 | return err 210 | } 211 | 212 | const updateTerminateAgentByID = `-- name: UpdateTerminateAgentByID :exec 213 | UPDATE agents 214 | SET status = 'terminated', number_of_active_calculations = 0 215 | WHERE agent_id = $1 216 | ` 217 | 218 | func (q *Queries) UpdateTerminateAgentByID(ctx context.Context, agentID int32) error { 219 | _, err := q.db.ExecContext(ctx, updateTerminateAgentByID, agentID) 220 | return err 221 | } 222 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator/parser" 9 | ) 10 | 11 | func TestParseExpression(t *testing.T) { 12 | testCases := []struct { 13 | name string 14 | expression string 15 | wantedExpression string 16 | err error 17 | }{ 18 | { 19 | name: "Valid expression with parentheses", 20 | expression: "(3+4)*5", 21 | wantedExpression: "3 4 + 5 *", 22 | err: nil, 23 | }, 24 | { 25 | name: "Valid expression with unary minus", 26 | expression: "-3+4*5", 27 | wantedExpression: "0 3 - 4 5 * +", 28 | err: nil, 29 | }, 30 | { 31 | name: "Invalid expression with mismatched parentheses", 32 | expression: "(3+4*5", 33 | wantedExpression: "", 34 | err: errors.New("invalid expression"), 35 | }, 36 | { 37 | name: "Invalid expression with invalid characters", 38 | expression: "3+x+4*5", 39 | wantedExpression: "", 40 | err: errors.New("invalid expression"), 41 | }, 42 | { 43 | name: "Expression with leading spaces", 44 | expression: " 3+4*5", 45 | wantedExpression: "3 4 5 * +", 46 | err: nil, 47 | }, 48 | { 49 | name: "Expression with trailing spaces", 50 | expression: "3+4*5 ", 51 | wantedExpression: "3 4 5 * +", 52 | err: nil, 53 | }, 54 | { 55 | name: "Expression with spaces in between", 56 | expression: "3 + 4 * 5", 57 | wantedExpression: "3 4 5 * +", 58 | err: nil, 59 | }, 60 | { 61 | name: "Expression with unary minus", 62 | expression: "-3+-4*5", 63 | wantedExpression: "0 3 - 0 + 4 5 * -", 64 | err: nil, 65 | }, 66 | { 67 | name: "Expression with six unary minus", 68 | expression: "------3", 69 | wantedExpression: "", 70 | err: errors.New("invalid expression"), 71 | }, 72 | { 73 | name: "Valid expression with unary plus", 74 | expression: "+3+4*5", 75 | wantedExpression: "0 3 + 4 5 * +", 76 | err: nil, 77 | }, 78 | { 79 | name: "Valid expression with consecutive operators", 80 | expression: "3++4*5", 81 | wantedExpression: "3 0 + 4 5 * +", 82 | err: nil, 83 | }, 84 | { 85 | name: "Expression with division by zero", 86 | expression: "3/0", 87 | wantedExpression: "", 88 | err: errors.New("invalid expression"), 89 | }, 90 | { 91 | name: "Expression with multiple operators", 92 | expression: "3+4*5-6/2", 93 | wantedExpression: "3 4 5 * + 6 2 / -", 94 | err: nil, 95 | }, 96 | { 97 | name: "Expression with excessive parentheses", 98 | expression: "(((3+4)*5)-6)/2", 99 | wantedExpression: "3 4 + 5 * 6 - 2 /", 100 | err: nil, 101 | }, 102 | { 103 | name: "Expression with starting unary minus and excessive parentheses", 104 | expression: "-(((3+4)*5)-6)/2", 105 | wantedExpression: "0 3 4 + 5 * 6 - 2 / -", 106 | err: nil, 107 | }, 108 | { 109 | name: "Expression with empty input", 110 | expression: "", 111 | wantedExpression: "", 112 | err: errors.New("invalid expression"), 113 | }, 114 | { 115 | name: "Expression with single number", 116 | expression: "42", 117 | wantedExpression: "42", 118 | err: nil, 119 | }, 120 | } 121 | 122 | for _, tc := range testCases { 123 | tc := tc 124 | t.Run(tc.name, func(t *testing.T) { 125 | got, err := parser.ParseExpression(tc.expression) 126 | if got != tc.wantedExpression { 127 | t.Errorf( 128 | "ParseExpression(%v) = %v, %v; want %v, but got %v", 129 | tc.expression, 130 | got, err, 131 | tc.wantedExpression, got, 132 | ) 133 | } 134 | if tc.err != nil && (err == nil || !strings.Contains(err.Error(), tc.err.Error())) { 135 | t.Errorf( 136 | "ParseExpression(%v) = %v, %v; expected error containing '%v', but got %v", 137 | tc.expression, 138 | got, err, 139 | tc.err, err, 140 | ) 141 | } else if tc.err == nil && err != nil { 142 | t.Errorf( 143 | "ParseExpression(%v) = %v, %v; expected no error, but got %v", 144 | tc.expression, 145 | got, err, 146 | err, 147 | ) 148 | } 149 | }) 150 | } 151 | 152 | } 153 | 154 | func TestIsValidExpression(t *testing.T) { 155 | testCases := []struct { 156 | name string 157 | expression string 158 | want bool 159 | }{ 160 | { 161 | name: "Empty expression", 162 | expression: "", 163 | want: false, 164 | }, 165 | { 166 | name: "Simple valid expression", 167 | expression: "(3+4)*5", 168 | want: true, 169 | }, 170 | { 171 | name: "Valid expression with nested parentheses", 172 | expression: "((3+4)*5)", 173 | want: true, 174 | }, 175 | { 176 | name: "Valid expression with multiple operators", 177 | expression: "3+4*5/2", 178 | want: true, 179 | }, 180 | { 181 | name: "Valid expression: unary operator at the beginning", 182 | expression: "3+-4*5", 183 | want: true, 184 | }, 185 | { 186 | name: "Valid expression: unary plus", 187 | expression: "3+4*5++2", 188 | want: true, 189 | }, 190 | { 191 | name: "Invalid expression: incomplete expression", 192 | expression: "3+4*", 193 | want: false, 194 | }, 195 | { 196 | name: "Invalid expression: division by zero", 197 | expression: "3+4/0", 198 | want: false, 199 | }, 200 | { 201 | name: "Invalid expression: unbalanced parentheses", 202 | expression: "3+(4*5", 203 | want: false, 204 | }, 205 | { 206 | name: "Valid expression with multiple parentheses", 207 | expression: "3+(4*(5-6)*2)/2", 208 | want: true, 209 | }, 210 | { 211 | name: "Two unary operators", 212 | expression: "--3+--4*5", 213 | want: false, 214 | }, 215 | { 216 | name: "Valid expression with leading zero in a number", 217 | expression: "03+4*5", 218 | want: false, 219 | }, 220 | { 221 | name: "Invalid expression: operator at the end", 222 | expression: "3+4*5+", 223 | want: false, 224 | }, 225 | { 226 | name: "Valid expression with negative number", 227 | expression: "3+(-4)*5", 228 | want: true, 229 | }, 230 | { 231 | name: "Valid expression: division by negative number", 232 | expression: "3+4/(-2)", 233 | want: true, 234 | }, 235 | { 236 | name: "Valid expression with negative number in parentheses", 237 | expression: "3+(-(4 * 5))", 238 | want: false, 239 | }, 240 | { 241 | name: "Invalid expression: incomplete negative number", 242 | expression: "3+(-)", 243 | want: false, 244 | }, 245 | } 246 | 247 | for _, tc := range testCases { 248 | tc := tc 249 | t.Run(tc.name, func(t *testing.T) { 250 | got := parser.IsValidExpression(tc.expression) 251 | if got != tc.want { 252 | t.Errorf("IsValidExpression(%v) = %v; want %v", tc.expression, got, tc.want) 253 | } 254 | }) 255 | } 256 | } 257 | 258 | func TestAddBrackets(t *testing.T) { 259 | testCases := []struct { 260 | name string 261 | expression string 262 | wantedExpression string 263 | }{ 264 | { 265 | name: "Unary plus at the beginning", 266 | expression: "+3+4*5", 267 | wantedExpression: "(0+3)+4*5", 268 | }, 269 | { 270 | name: "Unary minus at the beginning", 271 | expression: "-3+4*5", 272 | wantedExpression: "(0-3)+4*5", 273 | }, 274 | { 275 | name: "Unary minus before number", 276 | expression: "3+-4*5", 277 | wantedExpression: "3+0-4*5", 278 | }, 279 | { 280 | name: "Unary plus and minus combined", 281 | expression: "-3+-4*5", 282 | wantedExpression: "(0-3)+0-4*5", 283 | }, 284 | { 285 | name: "Expression with brackets", 286 | expression: "3+(4*5)+6", 287 | wantedExpression: "3+(4*5)+6", 288 | }, 289 | { 290 | name: "Expression with many same operators", 291 | expression: "1+1+2+2+3+3", 292 | wantedExpression: "(1+1)+(2+2)+(3+3)", 293 | }, 294 | } 295 | 296 | for _, tc := range testCases { 297 | tc := tc 298 | t.Run(tc.name, func(t *testing.T) { 299 | got := parser.AddBrackets(tc.expression) 300 | if got != tc.wantedExpression { 301 | t.Errorf("AddBrackets(%v) = %v; want %v", tc.expression, got, tc.wantedExpression) 302 | } 303 | }) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /backend/internal/storage/postgres/expressions.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.25.0 4 | // source: expressions.sql 5 | 6 | package postgres 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | "time" 12 | ) 13 | 14 | const assignExpressionToAgent = `-- name: AssignExpressionToAgent :exec 15 | UPDATE expressions 16 | SET agent_id = $1 17 | WHERE expression_id = $2 18 | ` 19 | 20 | type AssignExpressionToAgentParams struct { 21 | AgentID sql.NullInt32 22 | ExpressionID int32 23 | } 24 | 25 | func (q *Queries) AssignExpressionToAgent(ctx context.Context, arg AssignExpressionToAgentParams) error { 26 | _, err := q.db.ExecContext(ctx, assignExpressionToAgent, arg.AgentID, arg.ExpressionID) 27 | return err 28 | } 29 | 30 | const createExpression = `-- name: CreateExpression :one 31 | INSERT INTO expressions 32 | (created_at, updated_at, data, parse_data, status, user_id) 33 | VALUES 34 | ($1, $2, $3, $4, $5, $6) 35 | RETURNING 36 | expression_id, user_id, agent_id, 37 | created_at, updated_at, data, parse_data, 38 | status, result, is_ready 39 | ` 40 | 41 | type CreateExpressionParams struct { 42 | CreatedAt time.Time 43 | UpdatedAt time.Time 44 | Data string 45 | ParseData string 46 | Status ExpressionStatus 47 | UserID int32 48 | } 49 | 50 | func (q *Queries) CreateExpression(ctx context.Context, arg CreateExpressionParams) (Expression, error) { 51 | row := q.db.QueryRowContext(ctx, createExpression, 52 | arg.CreatedAt, 53 | arg.UpdatedAt, 54 | arg.Data, 55 | arg.ParseData, 56 | arg.Status, 57 | arg.UserID, 58 | ) 59 | var i Expression 60 | err := row.Scan( 61 | &i.ExpressionID, 62 | &i.UserID, 63 | &i.AgentID, 64 | &i.CreatedAt, 65 | &i.UpdatedAt, 66 | &i.Data, 67 | &i.ParseData, 68 | &i.Status, 69 | &i.Result, 70 | &i.IsReady, 71 | ) 72 | return i, err 73 | } 74 | 75 | const getComputingExpressions = `-- name: GetComputingExpressions :many 76 | SELECT 77 | expression_id, user_id, agent_id, 78 | created_at, updated_at, data, parse_data, 79 | status, result, is_ready 80 | FROM expressions 81 | WHERE status IN ('ready_for_computation', 'computing', 'terminated') 82 | ORDER BY created_at DESC 83 | ` 84 | 85 | func (q *Queries) GetComputingExpressions(ctx context.Context) ([]Expression, error) { 86 | rows, err := q.db.QueryContext(ctx, getComputingExpressions) 87 | if err != nil { 88 | return nil, err 89 | } 90 | defer rows.Close() 91 | var items []Expression 92 | for rows.Next() { 93 | var i Expression 94 | if err := rows.Scan( 95 | &i.ExpressionID, 96 | &i.UserID, 97 | &i.AgentID, 98 | &i.CreatedAt, 99 | &i.UpdatedAt, 100 | &i.Data, 101 | &i.ParseData, 102 | &i.Status, 103 | &i.Result, 104 | &i.IsReady, 105 | ); err != nil { 106 | return nil, err 107 | } 108 | items = append(items, i) 109 | } 110 | if err := rows.Close(); err != nil { 111 | return nil, err 112 | } 113 | if err := rows.Err(); err != nil { 114 | return nil, err 115 | } 116 | return items, nil 117 | } 118 | 119 | const getExpressionByID = `-- name: GetExpressionByID :one 120 | SELECT 121 | expression_id, user_id, agent_id, 122 | created_at, updated_at, data, parse_data, 123 | status, result, is_ready 124 | FROM expressions 125 | WHERE expression_id = $1 126 | ` 127 | 128 | func (q *Queries) GetExpressionByID(ctx context.Context, expressionID int32) (Expression, error) { 129 | row := q.db.QueryRowContext(ctx, getExpressionByID, expressionID) 130 | var i Expression 131 | err := row.Scan( 132 | &i.ExpressionID, 133 | &i.UserID, 134 | &i.AgentID, 135 | &i.CreatedAt, 136 | &i.UpdatedAt, 137 | &i.Data, 138 | &i.ParseData, 139 | &i.Status, 140 | &i.Result, 141 | &i.IsReady, 142 | ) 143 | return i, err 144 | } 145 | 146 | const getExpressionWithStatusComputing = `-- name: GetExpressionWithStatusComputing :many 147 | SELECT 148 | expression_id, user_id, agent_id, 149 | created_at, updated_at, data, parse_data, 150 | status, result, is_ready 151 | FROM expressions 152 | WHERE status = 'computing' 153 | ORDER BY created_at DESC 154 | ` 155 | 156 | func (q *Queries) GetExpressionWithStatusComputing(ctx context.Context) ([]Expression, error) { 157 | rows, err := q.db.QueryContext(ctx, getExpressionWithStatusComputing) 158 | if err != nil { 159 | return nil, err 160 | } 161 | defer rows.Close() 162 | var items []Expression 163 | for rows.Next() { 164 | var i Expression 165 | if err := rows.Scan( 166 | &i.ExpressionID, 167 | &i.UserID, 168 | &i.AgentID, 169 | &i.CreatedAt, 170 | &i.UpdatedAt, 171 | &i.Data, 172 | &i.ParseData, 173 | &i.Status, 174 | &i.Result, 175 | &i.IsReady, 176 | ); err != nil { 177 | return nil, err 178 | } 179 | items = append(items, i) 180 | } 181 | if err := rows.Close(); err != nil { 182 | return nil, err 183 | } 184 | if err := rows.Err(); err != nil { 185 | return nil, err 186 | } 187 | return items, nil 188 | } 189 | 190 | const getExpressions = `-- name: GetExpressions :many 191 | SELECT 192 | expression_id, user_id, agent_id, 193 | created_at, updated_at, data, parse_data, 194 | status, result, is_ready 195 | FROM expressions 196 | WHERE user_id = $1 197 | ORDER BY created_at DESC 198 | ` 199 | 200 | func (q *Queries) GetExpressions(ctx context.Context, userID int32) ([]Expression, error) { 201 | rows, err := q.db.QueryContext(ctx, getExpressions, userID) 202 | if err != nil { 203 | return nil, err 204 | } 205 | defer rows.Close() 206 | var items []Expression 207 | for rows.Next() { 208 | var i Expression 209 | if err := rows.Scan( 210 | &i.ExpressionID, 211 | &i.UserID, 212 | &i.AgentID, 213 | &i.CreatedAt, 214 | &i.UpdatedAt, 215 | &i.Data, 216 | &i.ParseData, 217 | &i.Status, 218 | &i.Result, 219 | &i.IsReady, 220 | ); err != nil { 221 | return nil, err 222 | } 223 | items = append(items, i) 224 | } 225 | if err := rows.Close(); err != nil { 226 | return nil, err 227 | } 228 | if err := rows.Err(); err != nil { 229 | return nil, err 230 | } 231 | return items, nil 232 | } 233 | 234 | const getTerminatedExpressions = `-- name: GetTerminatedExpressions :many 235 | SELECT 236 | expression_id, user_id, agent_id, 237 | created_at, updated_at, data, parse_data, 238 | status, result, is_ready 239 | FROM expressions 240 | WHERE status = 'terminated' 241 | ORDER BY created_at DESC 242 | ` 243 | 244 | func (q *Queries) GetTerminatedExpressions(ctx context.Context) ([]Expression, error) { 245 | rows, err := q.db.QueryContext(ctx, getTerminatedExpressions) 246 | if err != nil { 247 | return nil, err 248 | } 249 | defer rows.Close() 250 | var items []Expression 251 | for rows.Next() { 252 | var i Expression 253 | if err := rows.Scan( 254 | &i.ExpressionID, 255 | &i.UserID, 256 | &i.AgentID, 257 | &i.CreatedAt, 258 | &i.UpdatedAt, 259 | &i.Data, 260 | &i.ParseData, 261 | &i.Status, 262 | &i.Result, 263 | &i.IsReady, 264 | ); err != nil { 265 | return nil, err 266 | } 267 | items = append(items, i) 268 | } 269 | if err := rows.Close(); err != nil { 270 | return nil, err 271 | } 272 | if err := rows.Err(); err != nil { 273 | return nil, err 274 | } 275 | return items, nil 276 | } 277 | 278 | const makeExpressionReady = `-- name: MakeExpressionReady :exec 279 | UPDATE expressions 280 | SET parse_data = $1, result = $2, updated_at = $3, is_ready = True, status = 'result' 281 | WHERE expression_id = $4 282 | ` 283 | 284 | type MakeExpressionReadyParams struct { 285 | ParseData string 286 | Result int32 287 | UpdatedAt time.Time 288 | ExpressionID int32 289 | } 290 | 291 | func (q *Queries) MakeExpressionReady(ctx context.Context, arg MakeExpressionReadyParams) error { 292 | _, err := q.db.ExecContext(ctx, makeExpressionReady, 293 | arg.ParseData, 294 | arg.Result, 295 | arg.UpdatedAt, 296 | arg.ExpressionID, 297 | ) 298 | return err 299 | } 300 | 301 | const makeExpressionsTerminated = `-- name: MakeExpressionsTerminated :exec 302 | UPDATE expressions 303 | SET status = 'terminated' 304 | WHERE agent_id = $1 AND is_ready = false 305 | ` 306 | 307 | func (q *Queries) MakeExpressionsTerminated(ctx context.Context, agentID sql.NullInt32) error { 308 | _, err := q.db.ExecContext(ctx, makeExpressionsTerminated, agentID) 309 | return err 310 | } 311 | 312 | const updateExpressionParseData = `-- name: UpdateExpressionParseData :exec 313 | UPDATE expressions 314 | SET parse_data = $1 315 | WHERE expression_id = $2 316 | ` 317 | 318 | type UpdateExpressionParseDataParams struct { 319 | ParseData string 320 | ExpressionID int32 321 | } 322 | 323 | func (q *Queries) UpdateExpressionParseData(ctx context.Context, arg UpdateExpressionParseDataParams) error { 324 | _, err := q.db.ExecContext(ctx, updateExpressionParseData, arg.ParseData, arg.ExpressionID) 325 | return err 326 | } 327 | 328 | const updateExpressionStatus = `-- name: UpdateExpressionStatus :exec 329 | UPDATE expressions 330 | SET status = $1 331 | WHERE expression_id = $2 332 | ` 333 | 334 | type UpdateExpressionStatusParams struct { 335 | Status ExpressionStatus 336 | ExpressionID int32 337 | } 338 | 339 | func (q *Queries) UpdateExpressionStatus(ctx context.Context, arg UpdateExpressionStatusParams) error { 340 | _, err := q.db.ExecContext(ctx, updateExpressionStatus, arg.Status, arg.ExpressionID) 341 | return err 342 | } 343 | -------------------------------------------------------------------------------- /backend/internal/agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "log/slog" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/brokers" 16 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 17 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 18 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 19 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 20 | 21 | "github.com/streadway/amqp" 22 | ) 23 | 24 | type Agent struct { 25 | postgres.Agent 26 | log *slog.Logger 27 | dbConfig *storage.Storage 28 | SimpleComputers chan *messages.ExpressionMessage 29 | mu *sync.Mutex 30 | kill context.CancelFunc 31 | } 32 | 33 | // NewAgent creates new Agent. 34 | func NewAgent( 35 | log *slog.Logger, 36 | dbCfg *storage.Storage, 37 | kill context.CancelFunc, 38 | ) (*Agent, error) { 39 | const fn = "agent.NewAgent" 40 | 41 | agentObj, err := dbCfg.Queries.CreateAgent(context.Background(), postgres.CreateAgentParams{ 42 | CreatedAt: time.Now().UTC(), 43 | NumberOfParallelCalculations: 5, 44 | LastPing: time.Now().UTC(), 45 | Status: "waiting", 46 | }) 47 | if err != nil { 48 | log.Error("can't create agent", slog.String("fn", fn), sl.Err(err)) 49 | 50 | return nil, err 51 | } 52 | 53 | log.Info("create agent succesfully", slog.String("fn", fn)) 54 | 55 | return &Agent{ 56 | Agent: agentObj, 57 | log: log, 58 | dbConfig: dbCfg, 59 | SimpleComputers: make(chan *messages.ExpressionMessage), 60 | mu: &sync.Mutex{}, 61 | kill: kill, 62 | }, nil 63 | } 64 | 65 | // GetSafelyNumberOfActiveCalculations gets NumberOfActiveCalculations with Lock. 66 | func (a *Agent) GetSafelyNumberOfActiveCalculations() int32 { 67 | a.mu.Lock() 68 | defer a.mu.Unlock() 69 | 70 | return a.NumberOfActiveCalculations 71 | } 72 | 73 | // GetSafelyNumberOfParallelCalculations gets NumberOfParallelCalculations with Lock. 74 | func (a *Agent) GetSafelyNumberOfParallelCalculations() int32 { 75 | a.mu.Lock() 76 | defer a.mu.Unlock() 77 | 78 | return a.NumberOfParallelCalculations 79 | } 80 | 81 | // Terminate changes agent status to terminate. 82 | func (a *Agent) Terminate() { 83 | const fn = "agent.Terminate" 84 | 85 | err := a.dbConfig.Queries.UpdateTerminateAgentByID(context.Background(), a.AgentID) 86 | if err != nil { 87 | a.log.Error("can't terminate agent", slog.String("fn", fn), slog.Int("agentID", int(a.AgentID)), sl.Err(err)) 88 | 89 | return 90 | } 91 | 92 | a.kill() 93 | } 94 | 95 | // Ping sends pings to queue. 96 | func (a *Agent) Ping(producer brokers.Producer) { 97 | const fn = "agent.Ping" 98 | 99 | log := a.log.With( 100 | slog.String("fn", fn), 101 | ) 102 | 103 | exprMsg := messages.ExpressionMessage{ 104 | IsPing: true, 105 | AgentID: a.AgentID, 106 | } 107 | err := producer.PublishExpressionMessage(&exprMsg) 108 | if err != nil { 109 | log.Error("can't send ping", sl.Err(err)) 110 | 111 | return 112 | } 113 | 114 | log.Info("agent sends ping to orchestrator", slog.Time("time", time.Now())) 115 | } 116 | 117 | // RunSimpleComputer parses messages.ExpressionMessage and run SimpleComputer. 118 | func (a *Agent) RunSimpleComputer(ctx context.Context, exprMsg *messages.ExpressionMessage) error { 119 | const fn = "agent.RunSimpleComputer" 120 | 121 | tokenSplit := strings.Split(exprMsg.Token, " ") 122 | if len(tokenSplit) != 3 { 123 | return fmt.Errorf("invalid token, fn: %s", fn) 124 | } 125 | oper := tokenSplit[2] 126 | if !(oper == "+" || oper == "-" || oper == "/" || oper == "*") { 127 | return fmt.Errorf("operation in token doesn't match any of these +, -, /, *, fn: %s", fn) 128 | } 129 | 130 | digit1, err := strconv.Atoi(tokenSplit[0]) 131 | if err != nil { 132 | return fmt.Errorf("can't convert int to str: %v, fn: %s", err, fn) 133 | } 134 | digit2, err := strconv.Atoi(tokenSplit[1]) 135 | if err != nil { 136 | return fmt.Errorf("can't convert int to str: %v, fn: %s", err, fn) 137 | } 138 | if int(exprMsg.UserID) == 0 { 139 | a.log.Warn("", slog.String("oper", oper), slog.Int("userID", int(exprMsg.UserID))) 140 | } 141 | time_for_oper, err := a.dbConfig.Queries.GetOperationTimeByType(ctx, postgres.GetOperationTimeByTypeParams{ 142 | OperationType: oper, 143 | UserID: exprMsg.UserID, 144 | }) 145 | if err != nil { 146 | return fmt.Errorf("can't get execution time by operation type: %v, fn: %s", err, fn) 147 | } 148 | 149 | timer := time.NewTimer(time.Duration(time_for_oper) * time.Second) 150 | 151 | go simpleComputer(exprMsg, digit1, digit2, oper, timer, a.SimpleComputers) 152 | 153 | err = a.dbConfig.Queries.IncrementNumberOfActiveCalculations(ctx, a.AgentID) 154 | if err != nil { 155 | return fmt.Errorf("can't increment number of active calculations: %v, fn: %s", err, fn) 156 | } 157 | 158 | return nil 159 | } 160 | 161 | // DecrementActiveComputers decrements NumberOfActiveCalculations and changes agent Status. 162 | func (a *Agent) DecrementActiveComputers(ctx context.Context) error { 163 | const fn = "agent.DecrementActiveComputers" 164 | 165 | err := a.dbConfig.Queries.DecrementNumberOfActiveCalculations(ctx, a.AgentID) 166 | if err != nil { 167 | return fmt.Errorf("can't decrement number of active calculations, fn: %s", fn) 168 | } 169 | atomic.AddInt32(&a.NumberOfActiveCalculations, -1) 170 | if a.GetSafelyNumberOfActiveCalculations() == 0 { 171 | err := a.dbConfig.Queries.UpdateAgentStatus( 172 | ctx, 173 | postgres.UpdateAgentStatusParams{ 174 | Status: "waiting", 175 | AgentID: a.AgentID, 176 | }) 177 | if err != nil { 178 | return fmt.Errorf("can't update agent status: %v, fn: %s", err, fn) 179 | } 180 | a.Status = "waiting" 181 | } else { 182 | err := a.dbConfig.Queries.UpdateAgentStatus( 183 | ctx, 184 | postgres.UpdateAgentStatusParams{ 185 | Status: "running", 186 | AgentID: a.AgentID, 187 | }) 188 | if err != nil { 189 | return fmt.Errorf("can't update agent status: %v, fn: %s", err, fn) 190 | } 191 | a.Status = "running" 192 | } 193 | 194 | return nil 195 | } 196 | 197 | // ChangeAgentStatusToRunningOrSleeping changes agent Status to "running" or "sleeping". 198 | func (a *Agent) ChangeAgentStatusToRunningOrSleeping(ctx context.Context) error { 199 | const fn = "agent.ChangeAgentStatusToRunningOrSleeping" 200 | 201 | if a.GetSafelyNumberOfActiveCalculations() == a.GetSafelyNumberOfParallelCalculations() { 202 | err := a.dbConfig.Queries.UpdateAgentStatus( 203 | ctx, 204 | postgres.UpdateAgentStatusParams{ 205 | Status: "sleeping", 206 | AgentID: a.AgentID, 207 | }) 208 | if err != nil { 209 | return fmt.Errorf("can't update agent status: %v, fn: %s", err, fn) 210 | } 211 | a.Status = "sleeping" 212 | } else if a.Status != "running" { 213 | err := a.dbConfig.Queries.UpdateAgentStatus( 214 | ctx, 215 | postgres.UpdateAgentStatusParams{ 216 | Status: "running", 217 | AgentID: a.AgentID, 218 | }) 219 | if err != nil { 220 | return fmt.Errorf("can't update agent status: %v, fn: %s", err, fn) 221 | } 222 | a.Status = "running" 223 | } 224 | 225 | return nil 226 | } 227 | 228 | // ChangeExpressionStatus changes expression status to newStatus. 229 | func (a *Agent) ChangeExpressionStatus(ctx context.Context, exprID int32, newStatus string) error { 230 | const fn = "agent.ChangeExpressionStatus" 231 | 232 | err := a.dbConfig.Queries.UpdateExpressionStatus( 233 | ctx, 234 | postgres.UpdateExpressionStatusParams{ 235 | ExpressionID: exprID, 236 | Status: postgres.ExpressionStatus(newStatus), 237 | }) 238 | if err != nil { 239 | return fmt.Errorf("can't update expression status: %v, fn: %s", err, fn) 240 | } 241 | return nil 242 | } 243 | 244 | // AssignToAgent assigns expression to agent. 245 | func (a *Agent) AssignToAgent(ctx context.Context, exprID int32) error { 246 | const fn = "agent.AssignToAgent" 247 | 248 | err := a.dbConfig.Queries.AssignExpressionToAgent(ctx, postgres.AssignExpressionToAgentParams{ 249 | AgentID: sql.NullInt32{Int32: a.AgentID, Valid: true}, 250 | ExpressionID: exprID, 251 | }) 252 | if err != nil { 253 | a.log.Error("can't assign expression to agent", slog.String("fn", fn), sl.Err(err)) 254 | } 255 | 256 | return nil 257 | } 258 | 259 | // ConsumeMessageFromComputers handles message from simple computers. 260 | // Producer publishes it to queue. 261 | func (a *Agent) ConsumeMessageFromComputers(ctx context.Context, result *messages.ExpressionMessage, producer brokers.Producer) { 262 | const fn = "agent.ConsumeMessageFromComputers" 263 | 264 | log := a.log.With( 265 | slog.String("fn", fn), 266 | ) 267 | 268 | log.Info("agent consumes message from computers", slog.Any("message", result)) 269 | 270 | result.AgentID = a.AgentID 271 | 272 | err := producer.PublishExpressionMessage(result) 273 | if err != nil { 274 | producer, err = producer.Reconnect() 275 | if err != nil { 276 | log.Error("agent error", sl.Err(err)) 277 | a.kill() 278 | return 279 | } 280 | err = producer.PublishExpressionMessage(result) 281 | if err != nil { 282 | log.Error("agent error", sl.Err(err)) 283 | a.kill() 284 | return 285 | } 286 | } 287 | 288 | err = a.DecrementActiveComputers(ctx) 289 | if err != nil { 290 | log.Error("agent error", sl.Err(err)) 291 | a.kill() 292 | return 293 | } 294 | } 295 | 296 | // ConsumeMessageFromOrchestrator hanldes message from Consumer. 297 | func (a *Agent) ConsumeMessageFromOrchestrator(ctx context.Context, msgFromOrchestrator amqp.Delivery) { 298 | const fn = "agent.ConsumeMessageFromOrchestrator" 299 | 300 | log := a.log.With( 301 | slog.String("fn", fn), 302 | ) 303 | 304 | log.Info("agent consumes msg from orchestrator", slog.String("message", string(msgFromOrchestrator.Body))) 305 | 306 | var exprMsg messages.ExpressionMessage 307 | if err := json.Unmarshal(msgFromOrchestrator.Body, &exprMsg); err != nil { 308 | log.Error("agent error: failed to parse JSON", sl.Err(err)) 309 | a.kill() 310 | return 311 | } 312 | 313 | err := msgFromOrchestrator.Ack(false) 314 | if err != nil { 315 | log.Error("agent error: error acknowledging message", sl.Err(err)) 316 | a.kill() 317 | return 318 | } 319 | 320 | if exprMsg.Kill { 321 | log.Error("kill by orchestrator") 322 | a.kill() 323 | return 324 | } 325 | 326 | log.Info("token", slog.Any("tokens", exprMsg.Token)) 327 | 328 | err = a.AssignToAgent(ctx, exprMsg.ExpressionID) 329 | if err != nil { 330 | log.Error("agent error", sl.Err(err)) 331 | a.kill() 332 | return 333 | } 334 | 335 | err = a.ChangeExpressionStatus(ctx, exprMsg.ExpressionID, "computing") 336 | if err != nil { 337 | log.Error("agent error", sl.Err(err)) 338 | a.kill() 339 | return 340 | } 341 | 342 | err = a.RunSimpleComputer(ctx, &exprMsg) 343 | if err != nil { 344 | log.Error("agent error", sl.Err(err)) 345 | a.kill() 346 | return 347 | } 348 | 349 | err = a.ChangeAgentStatusToRunningOrSleeping(ctx) 350 | if err != nil { 351 | log.Error("agent error", sl.Err(err)) 352 | a.kill() 353 | return 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /backend/internal/orchestrator/orchestrator.go: -------------------------------------------------------------------------------- 1 | package orchestrator 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "fmt" 8 | "log/slog" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/brokers" 14 | "github.com/Prrromanssss/DAEC-fullstack/internal/domain/messages" 15 | "github.com/Prrromanssss/DAEC-fullstack/internal/lib/logger/sl" 16 | "github.com/Prrromanssss/DAEC-fullstack/internal/orchestrator/parser" 17 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage" 18 | "github.com/Prrromanssss/DAEC-fullstack/internal/storage/postgres" 19 | 20 | "github.com/streadway/amqp" 21 | ) 22 | 23 | type Orchestrator struct { 24 | log *slog.Logger 25 | dbConfig *storage.Storage 26 | InactiveTimeForAgent int32 27 | mu *sync.Mutex 28 | kill context.CancelFunc 29 | } 30 | 31 | // NewOrchestrator creates new Orchestrator. 32 | func NewOrchestrator( 33 | log *slog.Logger, 34 | dbCfg *storage.Storage, 35 | inactiveTimeForAgent int32, 36 | kill context.CancelFunc, 37 | ) (*Orchestrator, error) { 38 | 39 | return &Orchestrator{ 40 | log: log, 41 | dbConfig: dbCfg, 42 | InactiveTimeForAgent: inactiveTimeForAgent, 43 | mu: &sync.Mutex{}, 44 | kill: kill, 45 | }, nil 46 | } 47 | 48 | // AddTask publish message to agents. 49 | func (o *Orchestrator) AddTask( 50 | expressionMessage messages.ExpressionMessage, 51 | producer brokers.Producer, 52 | ) { 53 | const fn = "orchestrator.AddTask" 54 | 55 | o.log.Info("orchestrator ready to publish message to queue") 56 | 57 | tokens := parser.GetTokens(o.log, expressionMessage.Expression) 58 | for _, token := range tokens { 59 | err := producer.PublishExpressionMessage(&messages.ExpressionMessage{ 60 | ExpressionID: expressionMessage.ExpressionID, 61 | Token: token, 62 | Expression: expressionMessage.Expression, 63 | UserID: expressionMessage.UserID, 64 | }) 65 | if err != nil { 66 | o.log.Error("can't publish token to queue", sl.Err(err), slog.String("fn", fn)) 67 | // TODO: think about it. Should I kill orchestrator? 68 | o.kill() 69 | } 70 | } 71 | } 72 | 73 | // ReloadComputingExpressions add not completed expressions again to queue. 74 | func (o *Orchestrator) ReloadComputingExpressions( 75 | ctx context.Context, 76 | producer brokers.Producer, 77 | ) error { 78 | const fn = "orchestrator.ReloadComputingExpressions" 79 | 80 | expressions, err := o.dbConfig.Queries.GetComputingExpressions(ctx) 81 | if err != nil { 82 | return fmt.Errorf("orhestrator Error: %v, fn: %s", err, fn) 83 | } 84 | 85 | for _, expr := range expressions { 86 | msgToQueue := messages.ExpressionMessage{ 87 | ExpressionID: expr.ExpressionID, 88 | Expression: expr.ParseData, 89 | UserID: expr.UserID, 90 | } 91 | o.AddTask(msgToQueue, producer) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // CheckPing checks pings from agents and terminates them if there were no pings from them. 98 | // Then if agent was terminated sends expressions by this agent again to queue 99 | func (o *Orchestrator) CheckPing(ctx context.Context, producer brokers.Producer) error { 100 | const fn = "orchestrator.CheckPing" 101 | 102 | log := o.log.With( 103 | slog.String("fn", fn), 104 | ) 105 | 106 | tx, err := o.dbConfig.DB.Begin() 107 | if err != nil { 108 | return err 109 | } 110 | 111 | qtx := o.dbConfig.Queries.WithTx(tx) 112 | 113 | agentIDs, err := qtx.TerminateAgents( 114 | ctx, 115 | strconv.Itoa(int(o.InactiveTimeForAgent)), 116 | ) 117 | if err != nil { 118 | log.Error("can't make agents terminated", sl.Err(err)) 119 | errRollback := tx.Rollback() 120 | if errRollback != nil { 121 | log.Error("can't rollback transaction") 122 | 123 | return errRollback 124 | } 125 | return err 126 | } 127 | 128 | if len(agentIDs) == 0 { 129 | log.Info("all agents are activate") 130 | err = tx.Commit() 131 | if err != nil { 132 | log.Error("can't commit transaction", sl.Err(err)) 133 | 134 | return err 135 | } 136 | 137 | return nil 138 | } 139 | 140 | for _, agentID := range agentIDs { 141 | err := qtx.MakeExpressionsTerminated(ctx, sql.NullInt32{ 142 | Int32: agentID, 143 | Valid: true, 144 | }) 145 | if err != nil { 146 | log.Error("can't make expressions by this agent terminated", 147 | slog.Int("agent ID", int(agentID)), sl.Err(err)) 148 | errRollback := tx.Rollback() 149 | if errRollback != nil { 150 | log.Error("can't rollback transaction") 151 | 152 | return errRollback 153 | } 154 | return err 155 | } 156 | } 157 | err = tx.Commit() 158 | if err != nil { 159 | log.Error("can't commit transaction", sl.Err(err)) 160 | 161 | return err 162 | } 163 | 164 | expressions, err := o.dbConfig.Queries.GetTerminatedExpressions(ctx) 165 | if err != nil { 166 | log.Error("can't get expressions", sl.Err(err)) 167 | 168 | return err 169 | } 170 | 171 | for _, expr := range expressions { 172 | msgToQueue := messages.ExpressionMessage{ 173 | ExpressionID: expr.ExpressionID, 174 | Expression: expr.ParseData, 175 | UserID: expr.UserID, 176 | } 177 | o.AddTask(msgToQueue, producer) 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // FindForgottenExpressions that aren't processed by anyone. 184 | func (o *Orchestrator) FindForgottenExpressions(ctx context.Context, producer brokers.Producer) error { 185 | const fn = "orchestrator.FindForgottenExpressions" 186 | 187 | log := o.log.With( 188 | slog.String("fn", fn), 189 | ) 190 | 191 | tx, err := o.dbConfig.DB.Begin() 192 | if err != nil { 193 | return err 194 | } 195 | 196 | qtx := o.dbConfig.Queries.WithTx(tx) 197 | 198 | agentIDs, err := qtx.GetBusyAgents(ctx) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | if len(agentIDs) != 0 { 204 | errCommit := tx.Commit() 205 | if errCommit != nil { 206 | log.Error("can't commit transaction") 207 | 208 | return errCommit 209 | } 210 | return nil 211 | } 212 | 213 | expressions, err := qtx.GetExpressionWithStatusComputing(ctx) 214 | if err != nil { 215 | log.Error("can't get expressions", sl.Err(err)) 216 | errRollback := tx.Rollback() 217 | if errRollback != nil { 218 | log.Error("can't rollback transaction") 219 | 220 | return errRollback 221 | } 222 | return err 223 | } 224 | 225 | err = tx.Commit() 226 | if err != nil { 227 | log.Error("can't commit transaction", sl.Err(err)) 228 | 229 | return err 230 | } 231 | 232 | if len(expressions) == 0 { 233 | return nil 234 | } 235 | 236 | for _, expr := range expressions { 237 | msgToQueue := messages.ExpressionMessage{ 238 | ExpressionID: expr.ExpressionID, 239 | Expression: expr.ParseData, 240 | UserID: expr.UserID, 241 | } 242 | o.AddTask(msgToQueue, producer) 243 | } 244 | 245 | return nil 246 | } 247 | 248 | // HandlePing accepts ping from agent. 249 | func (o *Orchestrator) HandlePing(ctx context.Context, agentID int32) error { 250 | const fn = "orchestrator.HandlePing" 251 | 252 | err := o.dbConfig.Queries.UpdateAgentLastPing( 253 | ctx, 254 | postgres.UpdateAgentLastPingParams{ 255 | AgentID: agentID, 256 | LastPing: time.Now().UTC(), 257 | }) 258 | if err != nil { 259 | return fmt.Errorf("can't update last ping: %v, fn: %s", err, fn) 260 | } 261 | 262 | return nil 263 | } 264 | 265 | // HandleExpression makes expressions ready or publishes it again to queue. 266 | func (o *Orchestrator) HandleExpression( 267 | ctx context.Context, 268 | exprMsg messages.ExpressionMessage, 269 | producer brokers.Producer, 270 | ) error { 271 | const fn = "orchestrator.HandleExpression" 272 | 273 | newResultAndToken, err := o.UpdateExpressionFromAgents(ctx, exprMsg) 274 | if err != nil { 275 | return fmt.Errorf("orchestrator error: %v, fn: %s", err, fn) 276 | } 277 | 278 | result, err := strconv.Atoi(newResultAndToken.Result) 279 | 280 | if err == nil && 281 | parser.IsNumber(newResultAndToken.Result) || 282 | (newResultAndToken.Result[0] == '-' && parser.IsNumber(newResultAndToken.Result[1:])) { 283 | err := o.UpdateExpressionToReady(ctx, result, exprMsg.ExpressionID) 284 | if err != nil { 285 | return fmt.Errorf("orchestrator error: %v, fn: %s", err, fn) 286 | } 287 | 288 | return nil 289 | } 290 | if newResultAndToken.Token != "" { 291 | err := producer.PublishExpressionMessage(&messages.ExpressionMessage{ 292 | ExpressionID: exprMsg.ExpressionID, 293 | Token: newResultAndToken.Token, 294 | Expression: newResultAndToken.Result, 295 | UserID: exprMsg.UserID, 296 | }) 297 | if err != nil { 298 | return fmt.Errorf("orchestrator error: %v, fn: %s", err, fn) 299 | } 300 | } 301 | 302 | return nil 303 | } 304 | 305 | // UpdateExpressionFromAgents parses expression with new token and updates it in the database. 306 | func (o *Orchestrator) UpdateExpressionFromAgents( 307 | ctx context.Context, 308 | exprMsg messages.ExpressionMessage, 309 | ) (messages.ResultAndTokenMessage, error) { 310 | const fn = "orchestrator.UpdateExpressionFromAgents" 311 | 312 | expression, err := o.dbConfig.Queries.GetExpressionByID( 313 | ctx, 314 | exprMsg.ExpressionID, 315 | ) 316 | if err != nil { 317 | return messages.ResultAndTokenMessage{}, 318 | fmt.Errorf("can't get expression by id: %v, fn: %s", err, fn) 319 | } 320 | 321 | resAndTokenMsg, err := parser.InsertResultToToken( 322 | expression.ParseData, 323 | exprMsg.Token, 324 | exprMsg.Result, 325 | ) 326 | if err != nil { 327 | return messages.ResultAndTokenMessage{}, 328 | fmt.Errorf("can't insert tokens to expression: %v, fn: %s", err, fn) 329 | } 330 | 331 | err = o.dbConfig.Queries.UpdateExpressionParseData( 332 | ctx, 333 | postgres.UpdateExpressionParseDataParams{ 334 | ExpressionID: exprMsg.ExpressionID, 335 | ParseData: resAndTokenMsg.Result, 336 | }) 337 | if err != nil { 338 | return messages.ResultAndTokenMessage{}, 339 | fmt.Errorf("can't update expression data: %v, fn: %s", err, fn) 340 | } 341 | 342 | return resAndTokenMsg, nil 343 | } 344 | 345 | // UpdateExpressionToReady updates expression to ready. 346 | func (o *Orchestrator) UpdateExpressionToReady( 347 | ctx context.Context, 348 | result int, 349 | exprID int32, 350 | ) error { 351 | const fn = "orchestrator.UpdateExpressionToReady" 352 | 353 | err := o.dbConfig.Queries.MakeExpressionReady( 354 | ctx, 355 | postgres.MakeExpressionReadyParams{ 356 | ParseData: "", 357 | Result: int32(result), 358 | UpdatedAt: time.Now().UTC(), 359 | ExpressionID: exprID, 360 | }) 361 | if err != nil { 362 | return fmt.Errorf("can't make expression ready: %v, fn: %s", err, fn) 363 | } 364 | 365 | return nil 366 | } 367 | 368 | // HandleMessagesFromAgents consumes message from agents. 369 | // If it is ping handle it with HandlePing method. 370 | // If it is expression handle it with HandleExpression method. 371 | func (o *Orchestrator) HandleMessagesFromAgents( 372 | ctx context.Context, 373 | msgFromAgents amqp.Delivery, 374 | producer brokers.Producer, 375 | ) error { 376 | const fn = "orchestrator.HandleMessagesFromAgents" 377 | 378 | log := o.log.With( 379 | slog.String("fn", fn), 380 | ) 381 | 382 | log.Info("orchestrator consumes message from agent", slog.String("msg", string(msgFromAgents.Body))) 383 | 384 | err := msgFromAgents.Ack(false) 385 | if err != nil { 386 | log.Error("error acknowledging message", sl.Err(err)) 387 | return err 388 | } 389 | 390 | var exprMsg messages.ExpressionMessage 391 | if err := json.Unmarshal(msgFromAgents.Body, &exprMsg); err != nil { 392 | log.Error("failed to parse JSON", sl.Err(err)) 393 | return err 394 | } 395 | 396 | if exprMsg.IsPing { 397 | err := o.HandlePing(ctx, exprMsg.AgentID) 398 | if err != nil { 399 | log.Error("orchestrator error", sl.Err(err)) 400 | return err 401 | } 402 | } else { 403 | err := o.HandleExpression(ctx, exprMsg, producer) 404 | if err != nil { 405 | log.Error("", sl.Err(err)) 406 | return err 407 | } 408 | } 409 | 410 | return nil 411 | } 412 | -------------------------------------------------------------------------------- /backend/internal/protos/gen/go/daec/daec.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc v4.25.3 5 | // source: daec/daec.proto 6 | 7 | package daecv1 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type RegisterRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Email of the user to register. 29 | Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Password of ther user to register. 30 | } 31 | 32 | func (x *RegisterRequest) Reset() { 33 | *x = RegisterRequest{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_daec_daec_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *RegisterRequest) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*RegisterRequest) ProtoMessage() {} 46 | 47 | func (x *RegisterRequest) ProtoReflect() protoreflect.Message { 48 | mi := &file_daec_daec_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead. 60 | func (*RegisterRequest) Descriptor() ([]byte, []int) { 61 | return file_daec_daec_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *RegisterRequest) GetEmail() string { 65 | if x != nil { 66 | return x.Email 67 | } 68 | return "" 69 | } 70 | 71 | func (x *RegisterRequest) GetPassword() string { 72 | if x != nil { 73 | return x.Password 74 | } 75 | return "" 76 | } 77 | 78 | type RegisterResponse struct { 79 | state protoimpl.MessageState 80 | sizeCache protoimpl.SizeCache 81 | unknownFields protoimpl.UnknownFields 82 | 83 | UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // User ID of the registered user. 84 | } 85 | 86 | func (x *RegisterResponse) Reset() { 87 | *x = RegisterResponse{} 88 | if protoimpl.UnsafeEnabled { 89 | mi := &file_daec_daec_proto_msgTypes[1] 90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 91 | ms.StoreMessageInfo(mi) 92 | } 93 | } 94 | 95 | func (x *RegisterResponse) String() string { 96 | return protoimpl.X.MessageStringOf(x) 97 | } 98 | 99 | func (*RegisterResponse) ProtoMessage() {} 100 | 101 | func (x *RegisterResponse) ProtoReflect() protoreflect.Message { 102 | mi := &file_daec_daec_proto_msgTypes[1] 103 | if protoimpl.UnsafeEnabled && x != nil { 104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 105 | if ms.LoadMessageInfo() == nil { 106 | ms.StoreMessageInfo(mi) 107 | } 108 | return ms 109 | } 110 | return mi.MessageOf(x) 111 | } 112 | 113 | // Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead. 114 | func (*RegisterResponse) Descriptor() ([]byte, []int) { 115 | return file_daec_daec_proto_rawDescGZIP(), []int{1} 116 | } 117 | 118 | func (x *RegisterResponse) GetUserId() int64 { 119 | if x != nil { 120 | return x.UserId 121 | } 122 | return 0 123 | } 124 | 125 | type LoginRequest struct { 126 | state protoimpl.MessageState 127 | sizeCache protoimpl.SizeCache 128 | unknownFields protoimpl.UnknownFields 129 | 130 | Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Email of the user to login. 131 | Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Password of ther user to login. 132 | } 133 | 134 | func (x *LoginRequest) Reset() { 135 | *x = LoginRequest{} 136 | if protoimpl.UnsafeEnabled { 137 | mi := &file_daec_daec_proto_msgTypes[2] 138 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 139 | ms.StoreMessageInfo(mi) 140 | } 141 | } 142 | 143 | func (x *LoginRequest) String() string { 144 | return protoimpl.X.MessageStringOf(x) 145 | } 146 | 147 | func (*LoginRequest) ProtoMessage() {} 148 | 149 | func (x *LoginRequest) ProtoReflect() protoreflect.Message { 150 | mi := &file_daec_daec_proto_msgTypes[2] 151 | if protoimpl.UnsafeEnabled && x != nil { 152 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 153 | if ms.LoadMessageInfo() == nil { 154 | ms.StoreMessageInfo(mi) 155 | } 156 | return ms 157 | } 158 | return mi.MessageOf(x) 159 | } 160 | 161 | // Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead. 162 | func (*LoginRequest) Descriptor() ([]byte, []int) { 163 | return file_daec_daec_proto_rawDescGZIP(), []int{2} 164 | } 165 | 166 | func (x *LoginRequest) GetEmail() string { 167 | if x != nil { 168 | return x.Email 169 | } 170 | return "" 171 | } 172 | 173 | func (x *LoginRequest) GetPassword() string { 174 | if x != nil { 175 | return x.Password 176 | } 177 | return "" 178 | } 179 | 180 | type LoginResponse struct { 181 | state protoimpl.MessageState 182 | sizeCache protoimpl.SizeCache 183 | unknownFields protoimpl.UnknownFields 184 | 185 | Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // ID token of the logged user. 186 | } 187 | 188 | func (x *LoginResponse) Reset() { 189 | *x = LoginResponse{} 190 | if protoimpl.UnsafeEnabled { 191 | mi := &file_daec_daec_proto_msgTypes[3] 192 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 193 | ms.StoreMessageInfo(mi) 194 | } 195 | } 196 | 197 | func (x *LoginResponse) String() string { 198 | return protoimpl.X.MessageStringOf(x) 199 | } 200 | 201 | func (*LoginResponse) ProtoMessage() {} 202 | 203 | func (x *LoginResponse) ProtoReflect() protoreflect.Message { 204 | mi := &file_daec_daec_proto_msgTypes[3] 205 | if protoimpl.UnsafeEnabled && x != nil { 206 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 207 | if ms.LoadMessageInfo() == nil { 208 | ms.StoreMessageInfo(mi) 209 | } 210 | return ms 211 | } 212 | return mi.MessageOf(x) 213 | } 214 | 215 | // Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead. 216 | func (*LoginResponse) Descriptor() ([]byte, []int) { 217 | return file_daec_daec_proto_rawDescGZIP(), []int{3} 218 | } 219 | 220 | func (x *LoginResponse) GetToken() string { 221 | if x != nil { 222 | return x.Token 223 | } 224 | return "" 225 | } 226 | 227 | var File_daec_daec_proto protoreflect.FileDescriptor 228 | 229 | var file_daec_daec_proto_rawDesc = []byte{ 230 | 0x0a, 0x0f, 0x64, 0x61, 0x65, 0x63, 0x2f, 0x64, 0x61, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 231 | 0x6f, 0x12, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x43, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, 232 | 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 233 | 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 234 | 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 235 | 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x2b, 0x0a, 0x10, 236 | 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 237 | 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 238 | 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, 239 | 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 240 | 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 241 | 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 242 | 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, 0x0a, 0x0d, 0x4c, 243 | 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 244 | 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 245 | 0x65, 0x6e, 0x32, 0x73, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x39, 0x0a, 0x08, 0x52, 0x65, 246 | 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, 247 | 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 248 | 0x61, 0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 249 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 250 | 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 251 | 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 252 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x1d, 0x5a, 0x1b, 0x70, 0x72, 0x72, 0x72, 0x6f, 253 | 0x6d, 0x61, 0x6e, 0x73, 0x73, 0x73, 0x73, 0x2e, 0x64, 0x61, 0x65, 0x63, 0x2e, 0x76, 0x31, 0x3b, 254 | 0x64, 0x61, 0x65, 0x63, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 255 | } 256 | 257 | var ( 258 | file_daec_daec_proto_rawDescOnce sync.Once 259 | file_daec_daec_proto_rawDescData = file_daec_daec_proto_rawDesc 260 | ) 261 | 262 | func file_daec_daec_proto_rawDescGZIP() []byte { 263 | file_daec_daec_proto_rawDescOnce.Do(func() { 264 | file_daec_daec_proto_rawDescData = protoimpl.X.CompressGZIP(file_daec_daec_proto_rawDescData) 265 | }) 266 | return file_daec_daec_proto_rawDescData 267 | } 268 | 269 | var file_daec_daec_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 270 | var file_daec_daec_proto_goTypes = []interface{}{ 271 | (*RegisterRequest)(nil), // 0: auth.RegisterRequest 272 | (*RegisterResponse)(nil), // 1: auth.RegisterResponse 273 | (*LoginRequest)(nil), // 2: auth.LoginRequest 274 | (*LoginResponse)(nil), // 3: auth.LoginResponse 275 | } 276 | var file_daec_daec_proto_depIdxs = []int32{ 277 | 0, // 0: auth.Auth.Register:input_type -> auth.RegisterRequest 278 | 2, // 1: auth.Auth.Login:input_type -> auth.LoginRequest 279 | 1, // 2: auth.Auth.Register:output_type -> auth.RegisterResponse 280 | 3, // 3: auth.Auth.Login:output_type -> auth.LoginResponse 281 | 2, // [2:4] is the sub-list for method output_type 282 | 0, // [0:2] is the sub-list for method input_type 283 | 0, // [0:0] is the sub-list for extension type_name 284 | 0, // [0:0] is the sub-list for extension extendee 285 | 0, // [0:0] is the sub-list for field type_name 286 | } 287 | 288 | func init() { file_daec_daec_proto_init() } 289 | func file_daec_daec_proto_init() { 290 | if File_daec_daec_proto != nil { 291 | return 292 | } 293 | if !protoimpl.UnsafeEnabled { 294 | file_daec_daec_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 295 | switch v := v.(*RegisterRequest); i { 296 | case 0: 297 | return &v.state 298 | case 1: 299 | return &v.sizeCache 300 | case 2: 301 | return &v.unknownFields 302 | default: 303 | return nil 304 | } 305 | } 306 | file_daec_daec_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 307 | switch v := v.(*RegisterResponse); i { 308 | case 0: 309 | return &v.state 310 | case 1: 311 | return &v.sizeCache 312 | case 2: 313 | return &v.unknownFields 314 | default: 315 | return nil 316 | } 317 | } 318 | file_daec_daec_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 319 | switch v := v.(*LoginRequest); i { 320 | case 0: 321 | return &v.state 322 | case 1: 323 | return &v.sizeCache 324 | case 2: 325 | return &v.unknownFields 326 | default: 327 | return nil 328 | } 329 | } 330 | file_daec_daec_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 331 | switch v := v.(*LoginResponse); i { 332 | case 0: 333 | return &v.state 334 | case 1: 335 | return &v.sizeCache 336 | case 2: 337 | return &v.unknownFields 338 | default: 339 | return nil 340 | } 341 | } 342 | } 343 | type x struct{} 344 | out := protoimpl.TypeBuilder{ 345 | File: protoimpl.DescBuilder{ 346 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 347 | RawDescriptor: file_daec_daec_proto_rawDesc, 348 | NumEnums: 0, 349 | NumMessages: 4, 350 | NumExtensions: 0, 351 | NumServices: 1, 352 | }, 353 | GoTypes: file_daec_daec_proto_goTypes, 354 | DependencyIndexes: file_daec_daec_proto_depIdxs, 355 | MessageInfos: file_daec_daec_proto_msgTypes, 356 | }.Build() 357 | File_daec_daec_proto = out.File 358 | file_daec_daec_proto_rawDesc = nil 359 | file_daec_daec_proto_goTypes = nil 360 | file_daec_daec_proto_depIdxs = nil 361 | } 362 | --------------------------------------------------------------------------------