Merge feature/121-rabbitmq-queuing #122

Merged
aheathershaw merged 4 commits from feature/121-rabbitmq-queuing into master 2019-07-09 23:07:27 +01:00
9 changed files with 323 additions and 0 deletions
Showing only changes of commit 961603acd5 - Show all commits

View File

@ -0,0 +1,114 @@
<?php
namespace App\Console\Commands;
use App\Facade\UserConfig;
use App\Photo;
use App\QueueItem;
use App\Services\PhotoService;
use App\Services\RabbitMQService;
use Illuminate\Console\Command;
class ProcessQueueCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'queue:process';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Processes items in the processing queue.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!UserConfig::isImageProcessingQueueEnabled())
{
$this->output->error('The image processing queue is not enabled');
}
$rabbitmq = new RabbitMQService();
$this->output->writeln('Monitoring queue');
$rabbitmq->waitOnQueue([$this, 'processQueueItem']);
}
/**
* Processes a single item from the queue.
*
* @param $msg
* @return void
*/
public function processQueueItem($msg)
{
$queueItemID = intval($msg->body);
$this->output->writeln(sprintf('Processing queue item %d', $queueItemID));
/** @var QueueItem $queueItem */
$queueItem = QueueItem::where('id', $queueItemID)->first();
if (is_null($queueItem))
{
$this->output->writeln('Queue item does not exist; skipping');
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
return;
}
try
{
switch (strtolower($queueItem->action_type))
{
case 'photo.analyse':
$this->processPhotoAnalyseMessage($queueItem);
break;
default:
$this->output->writeln(sprintf('Action %s is not recognised, skipping', $queueItem->action_type));
break;
}
}
finally
{
$queueItem->completed_at = new \DateTime();
$queueItem->save();
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
}
}
private function processPhotoAnalyseMessage(QueueItem $queueItem)
{
$this->output->writeln(sprintf('Analysing photo ID %l (batch: %s)', $queueItem->photo_id, $queueItem->batch_reference));
$photo = Photo::where('id', $queueItem->photo_id)->first();
if (is_null($photo))
{
$this->output->writeln('Photo does not exist; skipping');
}
$photoService = new PhotoService($photo);
$photoService->analyse($queueItem->batch_reference);
}
}

View File

@ -123,6 +123,7 @@ class ConfigHelper
'rabbitmq_server' => 'localhost', 'rabbitmq_server' => 'localhost',
'rabbitmq_password' => encrypt('guest'), 'rabbitmq_password' => encrypt('guest'),
'rabbitmq_port' => 5672, 'rabbitmq_port' => 5672,
'rabbitmq_queue' => 'blue_twilight',
'rabbitmq_username' => 'guest', 'rabbitmq_username' => 'guest',
'recaptcha_enabled_registration' => false, 'recaptcha_enabled_registration' => false,
'recaptcha_secret_key' => '', 'recaptcha_secret_key' => '',
@ -199,6 +200,16 @@ class ConfigHelper
return $config; return $config;
} }
public function isImageProcessingQueueEnabled()
{
return $this->get('rabbitmq_enabled') &&
!empty($this->get('rabbitmq_server')) &&
!empty($this->get('rabbitmq_port')) &&
!empty($this->get('rabbitmq_username')) &&
!empty($this->get('rabbitmq_password')) &&
!empty($this->get('rabbitmq_queue'));
}
public function isSocialMediaLoginEnabled() public function isSocialMediaLoginEnabled()
{ {
return $this->get('social_facebook_login') || return $this->get('social_facebook_login') ||

View File

@ -67,6 +67,12 @@ class AlbumController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-albums'); $this->authorizeAccessToAdminPanel('admin:manage-albums');
$album = $this->loadAlbum($id, 'upload-photos'); $album = $this->loadAlbum($id, 'upload-photos');
if (UserConfig::isImageProcessingQueueEnabled())
{
dump($queue_token);exit();
}
$photos = $album->photos() $photos = $album->photos()
->where('is_analysed', false) ->where('is_analysed', false)
->orderBy('created_at') ->orderBy('created_at')

View File

@ -268,6 +268,7 @@ class DefaultController extends Controller
'rabbitmq_port', 'rabbitmq_port',
'rabbitmq_username', 'rabbitmq_username',
'rabbitmq_password', 'rabbitmq_password',
'rabbitmq_queue',
'sender_address', 'sender_address',
'sender_name', 'sender_name',
'smtp_server', 'smtp_server',

View File

@ -6,13 +6,16 @@ use App\Album;
use App\AlbumSources\IAlbumSource; use App\AlbumSources\IAlbumSource;
use App\Facade\Image; use App\Facade\Image;
use App\Facade\Theme; use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Helpers\FileHelper; use App\Helpers\FileHelper;
use App\Helpers\ImageHelper; use App\Helpers\ImageHelper;
use App\Helpers\MiscHelper; use App\Helpers\MiscHelper;
use App\Http\Requests\UpdatePhotosBulkRequest; use App\Http\Requests\UpdatePhotosBulkRequest;
use App\Label; use App\Label;
use App\Photo; use App\Photo;
use App\QueueItem;
use App\Services\PhotoService; use App\Services\PhotoService;
use App\Services\RabbitMQService;
use App\Upload; use App\Upload;
use App\UploadPhoto; use App\UploadPhoto;
use App\User; use App\User;
@ -304,6 +307,22 @@ class PhotoController extends Controller
// Log an activity record for the user's feed // Log an activity record for the user's feed
$this->createActivityRecord($photo, 'photo.uploaded'); $this->createActivityRecord($photo, 'photo.uploaded');
// If queueing is enabled, store the photo in the queue now
if (UserConfig::isImageProcessingQueueEnabled())
{
$queueItem = new QueueItem([
'batch_reference' => $queueUid,
'action_type' => 'photo.analyse',
'album_id' => $photo->album_id,
'photo_id' => $photo->id,
'queued_at' => new \DateTime()
]);
$queueItem->save();
$rabbitmq = new RabbitMQService();
$rabbitmq->queueItem($queueItem);
}
$isSuccessful = true; $isSuccessful = true;
} }
} }

21
app/QueueItem.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class QueueItem extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'batch_reference',
'action_type',
'album_id',
'photo_id',
'queued_at'
];
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Services;
use App\Facade\UserConfig;
use App\QueueItem;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
class RabbitMQService
{
protected $password;
protected $port;
protected $queue;
protected $server;
protected $username;
/**
* @var AMQPChannel
*/
private $channel;
/**
* @var AMQPStreamConnection
*/
private $connection;
public function __construct()
{
$this->server = UserConfig::get('rabbitmq_server');
$this->port = intval(UserConfig::get('rabbitmq_port'));
$this->username = UserConfig::get('rabbitmq_username');
$this->password = decrypt(UserConfig::get('rabbitmq_password'));
$this->queue = UserConfig::get('rabbitmq_queue');
}
public function queueItem(QueueItem $queueItem)
{
$this->connectAndInit();
try
{
$message = new AMQPMessage(
$queueItem->id,
array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT)
);
$this->channel->basic_publish($message, '', $this->queue);
}
finally
{
$this->disconnectAndCleanUp();
}
}
public function waitOnQueue($callback)
{
$this->connectAndInit();
try
{
$this->channel->basic_consume($this->queue, '', false, false, false, false, $callback);
while (count($this->channel->callbacks))
{
$this->channel->wait();
}
}
finally
{
$this->disconnectAndCleanUp();
}
}
private function connectAndInit()
{
$this->connection = new AMQPStreamConnection($this->server, $this->port, $this->username, $this->password);
$this->channel = $this->connection->channel();
$this->channel->queue_declare($this->queue, false, true, false, false);
}
private function disconnectAndCleanUp()
{
$this->channel->close();
$this->connection->close();
$this->channel = null;
$this->connection = null;
}
}

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateQueueItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('queue_items', function (Blueprint $table) {
$table->increments('id');
$table->string('batch_reference')->nullable(true);
$table->string('action_type', 20);
$table->unsignedInteger('album_id');
$table->unsignedBigInteger('photo_id');
$table->dateTime('queued_at');
$table->dateTime('completed_at')->nullable(true);
$table->timestamps();
$table->foreign('album_id')
->references('id')->on('albums')
->onDelete('cascade');
$table->foreign('photo_id')
->references('id')->on('photos')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('queue_items');
}
}

View File

@ -182,6 +182,17 @@
</div> </div>
@endif @endif
</div> </div>
<div class="form-group ml-4">
<label class="form-control-label" for="rabbitmq-queue">Queue:</label>
<input type="text" class="form-control{{ $errors->has('rabbitmq_queue') ? ' is-invalid' : '' }}" id="rabbitmq-queue" name="rabbitmq_queue" value="{{ old('rabbitmq_queue', $config['rabbitmq_queue']) }}">
@if ($errors->has('rabbitmq_queue'))
<div class="invalid-feedback">
<strong>{{ $errors->first('rabbitmq_queue') }}</strong>
</div>
@endif
</div>
</div> </div>
{{-- E-mail --}} {{-- E-mail --}}