diff --git a/README.md b/README.md index a56b6aa..cb3d944 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,105 @@ -# Тестовое задание +# Queue Server on Windows -## Очередь на go с REST интерфейсом +Это краткое руководство по запуску проекта на Windows. -Реализовать брокер очередей в виде веб сервиса. Сервис должен обрабатывать 2 метода: +--- -`PUT /queue/:queue` +## 1. Установка Go -```json -{ - "message": "data" -} +1. Скачайте и установите Go с официального сайта: [https://golang.org/dl/](https://golang.org/dl/). +2. Проверьте установку: + ```cmd + go version + ``` + Вы должны увидеть что-то вроде: + ``` + go version go1.20.5 windows/amd64 + ``` + +--- + +## 2. Клонирование проекта + +1. Создайте папку для проекта, например `C:\queue-server`. +2. Скопируйте все файлы проекта (`main.go`, `queue.go`, `server.go`) в эту папку. + +--- + +## 3. Компиляция программы + +Откройте командную строку (`cmd`) или PowerShell и перейдите в папку проекта: +```cmd +cd C:\queue-server +``` + +Скомпилируйте программу: +```cmd +go build -o queue-server.exe +``` + +--- + +## 4. Запуск сервера + +Запустите скомпилированный файл с параметрами: +```cmd +queue-server.exe -port=8080 -max-queues=10 -default-size=100 -timeout=5 +``` + +- `-port`: порт, на котором будет работать сервер (например, `8080`). +- `-max-queues`: максимальное количество очередей (например, `10`). +- `-default-size`: размер очереди по умолчанию (например, `100`). +- `-timeout`: таймаут ожидания сообщения в секундах (например, `5`). + +Если все настроено правильно, вы увидите сообщение: +``` +Starting server on port 8080 ``` -Положить сообщение message в очередь с именем `:queue` (имя очереди может быть любое), пример: +--- -`curl -XPUT http://localhost/queue/pet` -`curl -XPUT http://localhost/queue/role` +## 5. Тестирование -в ответ {пустое тело + статус 200 (ok)} -в случае если тело не по формату или отсутствует отдавать статус 400 (bad request) +Для тестирования можно использовать `curl` или Postman. -Желательно предусмотреть настройку максимального количества сообщений в очереди и общее количество очередей. +### Добавить сообщение в очередь: +```cmd +curl -XPUT http://localhost:8080/queue/pet -d "{\"message\":\"hello\"}" +``` -`GET /queue/:queue` +### Получить сообщение из очереди: +```cmd +curl http://localhost:8080/queue/pet?timeout=10 +``` -Забрать (по принципу FIFO) из очереди с названием `:queue` сообщение и вернуть в теле http запроса, пример (результат, который должен быть при выполненных put’ах выше) +--- -при GET-запросах сделать возможность задавать аргумент timeout +## 6. Альтернатива: Использование PowerShell -`curl http://localhost/queue/pet?timeout=N` +Если `curl` не работает, используйте PowerShell: -если в очереди нет готового сообщения получатель должен ждать либо до момента прихода сообщения либо до истечения таймаута (N - кол-во секунд). В случае, если сообщение так и не появилось - возвращать код 404. Получатели должны получать сообщения в том же порядке как от них поступал запрос, если 2 получателя ждут сообщения (используют таймаут), то первое сообщение должен получить тот, кто первый запросил. +### Добавить сообщение в очередь: +```powershell +curl -Method PUT -Uri "http://localhost:8080/queue/pet" -Body '{"message":"hello"}' -ContentType "application/json" +``` -Порт, на котором будет слушать сервис, должен задаваться в аргументах командной строки. -Настройка таймаута по умолчанию, должена задаваться или отключаться в аргументах командной строки как и остальные параметры. +### Получить сообщение из очереди: +```powershell +curl -Uri "http://localhost:8080/queue/pet?timeout=10" +``` -> Запрещается пользоваться какими либо сторонними пакетами кроме стандартных библиотек. (задача в написании кода, а не в использовании чужого) +--- -Желательно организовать код используя принципы гексоганальной архитектуры. -Комментарии приветствуются и помогут нам понять ход Ваших мыслей при разработке. +## 7. Возможные проблемы -Лаконичность кода будет восприниматься крайне положительно, не нужна "гибкость" больше, чем требуется для решения именно этой задачи, не нужны логи процесса работы программы (только обработка ошибок), никакого дебага и т.д... чем меньше кода - тем лучше! +1. **Go не найден**: + - Убедитесь, что Go добавлен в переменную среды `PATH`. Проверьте это командой: + ```cmd + echo %PATH% + ``` + - Если Go отсутствует в `PATH`, добавьте путь к папке `bin` Go (например, `C:\Go\bin`) в системные переменные среды. -Оцениваться корректность реализации (заданные условия выполняются),архитектурная составляющая (нет лишних действий в программе, только решающие задачи программы), лаконичность и понятность кода (субъективно, конечно, но думайте о том, насколько будет понятен ваш. код для других, это куда более важно в командной разработке, чем сложный "крутой" код). +2. **Проблемы с curl**: + - Если `curl` не работает, скачайте его с [https://curl.se/download.html](https://curl.se/download.html) или используйте PowerShell. -Результаты прислать пул-реквестом в данный репозиторий. +--- \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0df3d78 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go-queue-broker + +go 1.24.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..394cabe --- /dev/null +++ b/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "flag" + "log" + "time" +) + +func main() { + // Обрабатываются аргументы командной строки + port := flag.String("port", "8080", "port to listen on") + maxQueues := flag.Int("max-queues", 10, "maximum number of queues") + defaultSize := flag.Int("default-size", 100, "default queue size") + timeout := flag.Int("timeout", 5, "default timeout in seconds") + flag.Parse() + // Создается менеджер очередей и HTTP-сервер, запускается сервер + queueManager := NewQueueManager(*maxQueues, *defaultSize) + server := NewServer(queueManager, *port, time.Duration(*timeout)*time.Second) + + log.Printf("Starting server on port %s", *port) + if err := server.Start(); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/queue-server.exe b/queue-server.exe new file mode 100644 index 0000000..acd8ed7 Binary files /dev/null and b/queue-server.exe differ diff --git a/queue.go b/queue.go new file mode 100644 index 0000000..fbf8a6c --- /dev/null +++ b/queue.go @@ -0,0 +1,131 @@ +package main + +import ( + "errors" + "sync" + "time" +) + +// Queue представляет одну очередь сообщений. +type Queue struct { + messages []string + maxSize int + mu sync.Mutex + waiters []*waiter +} + +// waiter представляет ожидающего получателя. +type waiter struct { + ch chan string +} + +// NewQueue создает новую очередь с максимальным размером maxSize. +func NewQueue(maxSize int) *Queue { + return &Queue{ + messages: []string{}, + maxSize: maxSize, + } +} + +// Add добавляет сообщение в очередь. +func (q *Queue) Add(message string) error { + q.mu.Lock() + defer q.mu.Unlock() + + if len(q.messages) >= q.maxSize { + return errors.New("queue is full") + } + + q.messages = append(q.messages, message) + q.notifyWaiters() + return nil +} + +// Get извлекает сообщение из очереди по принципу FIFO. +func (q *Queue) Get(timeout time.Duration) (string, error) { + q.mu.Lock() + defer q.mu.Unlock() + + if len(q.messages) > 0 { + message := q.messages[0] + q.messages = q.messages[1:] + return message, nil + } + + if timeout == 0 { + return "", errors.New("no messages available") + } + + // Создаем нового ожидающего получателя. + waiter := &waiter{ch: make(chan string)} + q.waiters = append(q.waiters, waiter) + q.mu.Unlock() + + select { + case message := <-waiter.ch: + q.mu.Lock() + return message, nil + case <-time.After(timeout): + q.mu.Lock() + q.removeWaiter(waiter) + return "", errors.New("timeout waiting for message") + } +} + +// notifyWaiters уведомляет всех ожидающих получателей о новом сообщении. +func (q *Queue) notifyWaiters() { + for len(q.waiters) > 0 && len(q.messages) > 0 { + // Переименуем переменную waiter в w, чтобы избежать конфликта имен. + w := q.waiters[0] + q.waiters = q.waiters[1:] + message := q.messages[0] + q.messages = q.messages[1:] + go func(waiter *waiter, msg string) { + waiter.ch <- msg + }(w, message) + } +} + +// removeWaiter удаляет ожидающего получателя из списка. +func (q *Queue) removeWaiter(w *waiter) { + for i, waiter := range q.waiters { + if waiter == w { + q.waiters = append(q.waiters[:i], q.waiters[i+1:]...) + break + } + } +} + +// QueueManager управляет всеми очередями. +type QueueManager struct { + queues map[string]*Queue + maxQueues int + defaultSize int + mu sync.Mutex +} + +// NewQueueManager создает новый менеджер очередей. +func NewQueueManager(maxQueues, defaultSize int) *QueueManager { + return &QueueManager{ + queues: make(map[string]*Queue), + maxQueues: maxQueues, + defaultSize: defaultSize, + } +} + +// GetOrCreateQueue возвращает существующую очередь или создает новую. +func (qm *QueueManager) GetOrCreateQueue(name string) (*Queue, error) { + qm.mu.Lock() + defer qm.mu.Unlock() + + if len(qm.queues) >= qm.maxQueues { + return nil, errors.New("maximum number of queues reached") + } + + queue, exists := qm.queues[name] + if !exists { + queue = NewQueue(qm.defaultSize) + qm.queues[name] = queue + } + return queue, nil +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..8ebad23 --- /dev/null +++ b/server.go @@ -0,0 +1,105 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" +) + +// Server представляет HTTP-сервер. +type Server struct { + queueManager *QueueManager + port string + timeout time.Duration +} + +// NewServer создает новый HTTP-сервер. +func NewServer(queueManager *QueueManager, port string, timeout time.Duration) *Server { + return &Server{ + queueManager: queueManager, + port: port, + timeout: timeout, + } +} + +// Start запускает сервер. +func (s *Server) Start() error { + http.HandleFunc("/queue/", s.handleQueue) + return http.ListenAndServe(":"+s.port, nil) +} + +// handleQueue обрабатывает запросы к очередям. +func (s *Server) handleQueue(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + if len(parts) != 3 { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + + queueName := parts[2] + + switch r.Method { + case http.MethodPut: + s.handlePut(w, r, queueName) + case http.MethodGet: + s.handleGet(w, r, queueName) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +// handlePut обрабатывает PUT-запросы. +func (s *Server) handlePut(w http.ResponseWriter, r *http.Request, queueName string) { + var body struct { + Message string `json:"message"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Message == "" { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + queue, err := s.queueManager.GetOrCreateQueue(queueName) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + + if err := queue.Add(body.Message); err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + + w.WriteHeader(http.StatusOK) +} + +// handleGet обрабатывает GET-запросы. +func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, queueName string) { + queue, err := s.queueManager.GetOrCreateQueue(queueName) + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + + timeout := s.timeout + if t := r.URL.Query().Get("timeout"); t != "" { + if parsedTimeout, err := strconv.Atoi(t); err == nil { + timeout = time.Duration(parsedTimeout) * time.Second + } + } + + message, err := queue.Get(timeout) + if err != nil { + if err.Error() == "timeout waiting for message" { + http.Error(w, "timeout", http.StatusNotFound) + } else { + http.Error(w, "no messages available", http.StatusNotFound) + } + return + } + + fmt.Fprint(w, message) +}