├── 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 |
50 |
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 | 
4 | 
5 |
6 | ## About
7 |
8 | **This is distributed arithmetic expression calculator.**
9 |
10 | 
11 | 
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 | 
120 |
121 | ## ER-diagram
122 | 
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 |
--------------------------------------------------------------------------------