Laravel: синхронизированная система очередей

Я пытаюсь настроить API, который использует систему очередей на другом сервере для обработки запросов. Позвольте мне начать то, что я пытаюсь выполнить без системы очередей (без авторизации для простоты): например, используя Postman, отправив запрос GET на URL-адрес https://example.com/products вернет строку JSON, например

[
    {
        "id": 1,
        "name": "some name",
        ...
    },
    {
        "id": 2,
        "name": "some other name",
        ...
    }.
    ...
]

Код в route/api.php будет выглядеть примерно так:

<php

Route::get('/products', ProductController@index');

И код в app/Http/Controllers/ProductController.php:

<?php

namespace App\Http\Controllers;

class ProductController extends Controller
{
    /**
     * Return the products.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        // Logic to get the products.
        return $products->toJson();
    }
}

Что я хотел бы сделать, так это то, что вся бизнес-логика обрабатывается на другом сервере, на котором работают несколько рабочих процессов. Далее следует мое обоснование этого.

  • Безопасность: в случае, если нас взломают, скорее всего, это будет клиентский сервер, а не рабочий сервер. Поскольку у последнего есть вся бизнес-логика, хакер в худшем случае сможет получить только данные входящего и исходящего запроса.
  • Несколько работников: Получение продуктов, вероятно, не займет много времени, но могут быть и другие запросы, для обработки которых требуется больше времени. В большинстве ситуаций пользователь, делающий запрос, должен будет дождаться результата. Однако другим пользователям, совершающим вызов, не следует этого ждать. Поэтому другой работник может принять этот запрос и выполнить задание.

Это будет рабочий процесс, как я его вижу:

  • Все свободные воркеры постоянно запрашивают задание в очереди

    1. User makes request
    2. Клиентский сервер принимает данные запроса и ставит их в очередь
    3. Рабочий берет задание из очереди и обрабатывает его
    4. Рабочий возвращает результат клиентскому серверу
    5. Клиент-сервер возвращает результат пользователю

Ниже небольшой рисунок, чтобы прояснить ситуацию.

  User 1
      _   \
     |     \
       \    \   1.
        \    \  request
         \    \                                  -------------
  result  \    \                                /             \
  5.       \    \                               |  Worker     |
            \    _|                             |  Server     |
             \                                  |  ---------  |
                   -------------                | /         \ |
                  /             \               | | Worker1 | |
                  |  Client     |            /  | |         | |  \
                  |  Server  2. |           /   | \         / |   \
                  |  ---------  |          /    |  ---------  |    \
                  | /         \ |         /     |  ---------  |     \
                  | | Queue   | |        /      | /         \ |      \     ---------
                  | |         | |      |_       | | Worker2 | |       _|  /         \
                  | | Job A   | |               | |         | |           | DB      |
                  | | Job B   | |   3.  <-----  | \         / |  ----->   | Server  |
                  | |         | |       _       |  ---------  |       _   |         |
                  | \         / |      |        |  ...        |        |  \         /
                  |  ---------  |        \      |  ---------  |      /     ---------
                  \             /         \     | /         \ |     /      ---------
                   -------------           \    | | WorkerN | |    /      /         \
              _               4. ?          \   | |         | |   /       | Other   |
               |                                | \         / |           | Servers |
             /    /                             |  ---------  |           |         |
  1.        /    /                              \             /           \         /
  request  /    /                                -------------             ---------
          /    /
         /    /  result
        /    /   5.
       /    /
          |_
   User 2

В документации Laravel я наткнулся на очереди, которые, как я думал, легко помогут. Я начал экспериментировать с Beanstalkd, но полагаю, что подойдет любой драйвер очереди. Проблема, с которой я столкнулся, заключается в том, что система очередей работает асинхронно. Как следствие, клиентский сервер просто продолжает работу, не дожидаясь результата. Если я что-то не упустил, похоже, нет возможности заставить систему очередей работать синхронно.

При дальнейшем изучении документации Laravel я наткнулся на трансляцию. Я не уверен, понимаю ли я концепцию трансляции на 100%, но из того, что я действительно понимаю, это то, что получение, похоже, происходит в Javascript. Я бэкэнд-разработчик и хотел бы держаться подальше от Javascript. По какой-то причине мне кажется неправильным использовать здесь javascript, однако я не уверен, что это чувство оправдано.

Глядя дальше в документацию, я наткнулся на Redis. В основном меня заинтриговала функциональность Pub/Sub. Я думал, что клиентский сервер может сгенерировать уникальное значение, отправить его с данными запроса в очередь и подписаться на него. После завершения рабочего процесса он может опубликовать результат с этим уникальным значением. Я думал, что это может покрыть недостающую часть для шага 4. Я до сих пор не уверен, как это будет работать в коде, если эта логика вообще будет работать. Я в основном застрял на той части, где клиент должен слушать и получать данные от Redis.

Я мог упустить что-то очень простое. Знайте, что я относительно новичок в программировании на PHP и в концепции программирования через всемирную паутину. Поэтому, если вы обнаружите, что логика ошибочна или слишком надумана, пожалуйста, дайте мне несколько указателей на другие/лучшие методы.

К вашему сведению, я слышал о Gearman, которые, кажется, могут работать как синхронно, так и асинхронно. Однако я хотел бы держаться подальше от этого, поскольку моя цель — использовать инструменты, предоставляемые Laravel, в полной мере. Я все еще учусь и недостаточно уверен, чтобы использовать слишком много внешних плагинов.

Правка: вот как далеко я продвинулся. Чего мне еще не хватает? Или то, что я прошу (почти) невозможно?

Пользователь звонит http://my.domain.com/worker?message=whoop

Пользователь должен получить ответ в формате JSON.

{"message":"you said whoop"}

Обратите внимание, что в заголовке ответа тип контента должен быть "application/json", а не "text/html; charset=UTF-8".

Это то, что у меня есть до сих пор:

Два сервера API-сервер и WORKER-сервер. API-сервер получает запросы и ставит их в очередь (локальный Redis). Рабочие на WORKER-сервере обрабатывают задания на API-сервере. Как только воркер обработал задание, результат этого задания транслируется на API-сервер. API-сервер прослушивает широковещательный ответ и отправляет его пользователю. Это делается с помощью Redis и socket.io. Моя проблема в том, что на данный момент, чтобы отправить результат, я отправляю блейд-файл с некоторым Javascript, который прослушивает ответ. Это приводит к типу содержимого "text/html; charset=UTF-8" с элементом, который обновляется после передачи результата от рабочего процесса. Есть ли способ вместо того, чтобы возвращать представление, заставить возврат «ждать», пока результат не будет передан?

API-сервер: route\web.php:

<?php

Route::get('/worker', 'ApiController@workerHello');

API-сервер: app\Http\Controllers\ApiController.php

<?php

namespace App\Http\Controllers;

use App\Jobs\WorkerHello;
use Illuminate\Http\Request;

class ApiController extends Controller
{
    /**
     * Dispatch on queue and open the response page
     *
     * @return string
     */
    public function workerHello(Request $request)
    {
        // Dispatch the request to the queue
        $jobId = $this->dispatch(new WorkerHello($request->message));

        return view('response', compact('jobId'));
    }
}

API-сервер: app\Jobs\WorkerHello.php

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class WorkerHello implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $message;

    /**
     * Create a new job instance.
     *
     * @param  string  $message  the message
     * @return void
     */
    public function __construct($message = null)
    {
        $this->message = $message;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {

    }
}

WORKER-сервер: app\Jobs\WorkerHello.php

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use App\Events\WorkerResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class WorkerHello implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $message;

    /**
     * Create a new job instance.
     *
     * @param  string  $message  the message
     * @return void
     */
    public function __construct($message = null)
    {
        $this->message = $message;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        // Do stuff
        $message = json_encode(['message' => 'you said ' . $this->message]);

        event(new WorkerResponse($this->job->getJobId(), $message));
    }
}

WORKER-сервер: app\Events\WorkerResponse.php

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class WorkerResponse implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    protected $jobId;
    public $message;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($jobId, $message)
    {
        $this->jobId = $jobId;
        $this->message = $message;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('worker-response.' . $this->jobId);
    }
}

API-сервер: socket.js (работает с узлом)

var server = require('http').Server();

var io = require('socket.io')(server);

var Redis = require('ioredis');
var redis = new Redis();

redis.psubscribe('worker-response.*');

redis.on('pmessage', function(pattern, channel, message) {
    message = JSON.parse(message);
    io.emit(channel + ':' + message.event, channel, message.data); 
});

server.listen(3000);

API-сервер: resources\views\response.blade.php

<!doctype html>
<html lang="{{ config('app.locale') }}">
    <head>
    </head>
    <body>
        <div id="app">
            <p>@{{ message }}</p>
        </div>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>

        <script>
            var socket = io('http://192.168.10.10:3000');

            new Vue({
                el: '#app',

                data: {
                    message: '',
                },

                mounted: function() {
                    socket.on('worker-response.{{ $jobId }}:App\\Events\\WorkerResponse', function (channel, data) {
                        this.message = data.message;
                    }.bind(this));
                }
            });
        </script>
    </body>
</html>

person Ruben Colpaert    schedule 08.05.2017    source источник
comment
Вау +1 за ascii-арт   -  person rap-2-h    schedule 08.05.2017
comment
Зачем нужны очереди синхронизации? Вы можете использовать события для передачи данных клиентам. В общем случае клиентский сервер может сделать что-то еще, когда он получил данные от события. Или я не понимаю, что вы хотите))   -  person mcklayin    schedule 08.05.2017
comment
@mcklayin, вы говорите о клиенте как о пользователе или о коде на клиентском сервере, который используется на рисунке? Если вы имеете в виду трансляцию, как это объясняется в документации Laravel, я понимаю, что клиентский код ожидает ответа на JavaScript, чего я хотел бы избежать, поскольку я хотел бы придерживаться PHP. Как упоминалось в моем первоначальном вопросе, я не слишком хорошо знаком с программированием для Интернета, а концепция вещания довольно туманна.   -  person Ruben Colpaert    schedule 08.05.2017
comment
Я думаю, что ваш вопрос лучше, но мой ответ по-прежнему соответствует моему мнению, см. предыдущий ответ здесь stackoverflow.com/questions/42955635/ Мне было трудно понять события, когда я начинал и, возможно, вы новичок в этом (это звучит грубо, но я пытаюсь помочь и быть милым :)). Попробуйте реализовать pusher, чтобы увидеть, как он работает, и если вы можете его использовать, существует много альтернатив, но pusher прост в использовании pusher. ком   -  person Markus Tenghamn    schedule 08.05.2017
comment
Вот хороший учебник для толкателя, но он для приложения чата, однако вам просто нужно удалить функциональность чата и адаптировать ее к вашим потребностям blog.pusher.com/how-to-build-a-laravel-chat-app-with-pusher   -  person Markus Tenghamn    schedule 08.05.2017
comment
@MarkusTenghamn, у меня есть несколько опасений по поводу подхода Pusher. Это создает еще один сервер, который еще немного замедлит работу API. Я думаю, это можно решить, используя Redis вместо Pusher, так что это не большая проблема. Однако есть ли способ сделать это с PHP, а не с JavaScript, Vue и Axios. Я хотел бы избежать изучения другого языка для чего-то, что кажется довольно простым. Поэтому я думаю, что мой главный вопрос будет заключаться в том, как это можно сделать только с помощью PHP? И, кроме того, возможно ли сделать это синхронно или, возможно, подделать (возможно, с функцией сна)?   -  person Ruben Colpaert    schedule 16.05.2017
comment
@RubenColpaert Ну, на мой взгляд, javascript был бы лучшим подходом, как и любой язык на стороне клиента, если на то пошло. Проблема в том, что php на стороне сервера. Вы можете прослушивать сокеты с помощью php, это просто загрузит страницу до тех пор, пока не будет получено сообщение, которое вы ищете. Я сделал Slack-бота с GuzzleHttp, который работает в течение часа, но я не должен был писать его на PHP. Возможно, эта ссылка на статью об асинхронных запросах php поможет, но я думаю, что вы можете столкнуться с большим количеством проблем таким образом segment.com/blog/how-to-make-async-requests-in-php   -  person Markus Tenghamn    schedule 16.05.2017
comment
@MarkusTenghamn, я добавил некоторую информацию о том, как далеко я продвинулся до сих пор. Я чувствую, что либо что-то упускаю, либо не до конца понимаю ваш поток.   -  person Ruben Colpaert    schedule 05.07.2017