Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base64 encode serialized jobs when using Postgres #51

Merged
merged 6 commits into from
Apr 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config/venture.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php declare(strict_types=1);

use Sassnowski\Venture\Serializer\Base64WorkflowSerializer;
use Sassnowski\Venture\ClassNameStepIdGenerator;
use Sassnowski\Venture\UnserializeJobExtractor;

Expand Down Expand Up @@ -30,4 +31,12 @@
* Needs to implement `Sassnowski\Venture\JobExtractor`.
*/
'workflow_job_extractor_class' => UnserializeJobExtractor::class,

/*
* The class that should be used to serialize and unserialize workflow jobs.
* In most cases, you won't have to change this.
*
* Needs to implement `Sassnowski\Venture\Serializer\WorkflowJobSerializer`.
*/
'workflow_job_serializer_class' => Base64WorkflowSerializer::class,
];
10 changes: 8 additions & 2 deletions src/Models/Workflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Opis\Closure\SerializableClosure;
use Sassnowski\Venture\Serializer\WorkflowJobSerializer;
use Sassnowski\Venture\Venture;
use Throwable;

Expand Down Expand Up @@ -70,7 +71,7 @@ public function jobs(): HasMany
public function addJobs(array $jobs): void
{
collect($jobs)->map(fn (array $job) => [
'job' => \serialize(clone $job['job']),
'job' => $this->serializer()->serialize(clone $job['job']),
'name' => $job['name'],
'uuid' => $job['job']->stepId,
'edges' => $job['job']->dependantJobs,
Expand Down Expand Up @@ -257,7 +258,7 @@ private function runDependantJobs(object $job): void
->whereIn('uuid', $job->dependantJobs)
->get('job')
->pluck('job')
->map(fn (string $job) => \unserialize($job));
->map(fn (string $job) => $this->serializer()->unserialize($job));
}

$dependantJobs
Expand All @@ -266,4 +267,9 @@ private function runDependantJobs(object $job): void
$this->dispatchJob($job);
});
}

private function serializer(): WorkflowJobSerializer
{
return Container::getInstance()->make(WorkflowJobSerializer::class);
}
}
55 changes: 55 additions & 0 deletions src/Serializer/Base64WorkflowSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2021 Kai Sassnowski
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/ksassnowski/venture
*/

namespace Sassnowski\Venture\Serializer;

use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\PostgresConnection;
use Illuminate\Support\Str;
use Laravel\SerializableClosure\Serializers\UnserializeException;

final class Base64WorkflowSerializer implements WorkflowJobSerializer
ksassnowski marked this conversation as resolved.
Show resolved Hide resolved
{
public function __construct(private ConnectionInterface $connection)
{
}

public function serialize(object $job): string
{
if ($this->isPostgresConnection()) {
return \base64_encode(\serialize($job));
}

return \serialize($job);
}

public function unserialize(string $serializedJob): object
{
if ($this->isPostgresConnection() && !Str::contains($serializedJob, [':', ';'])) {
$serializedJob = \base64_decode($serializedJob, true);
}

$result = @\unserialize($serializedJob);

if (false === $result) {
throw new UnserializeException('Unable to unserialize job');
}

return $result;
}

private function isPostgresConnection(): bool
{
return $this->connection instanceof PostgresConnection;
}
}
35 changes: 35 additions & 0 deletions src/Serializer/DefaultSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2021 Kai Sassnowski
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/ksassnowski/venture
*/

namespace Sassnowski\Venture\Serializer;

use Laravel\SerializableClosure\Serializers\UnserializeException;

final class DefaultSerializer implements WorkflowJobSerializer
ksassnowski marked this conversation as resolved.
Show resolved Hide resolved
{
public function serialize(object $job): string
{
return \serialize($job);
}

public function unserialize(string $serializedJob): object
{
$result = @\unserialize($serializedJob);

if (false === $result) {
throw new UnserializeException('Unable to unserialize job');
}

return $result;
}
}
26 changes: 26 additions & 0 deletions src/Serializer/WorkflowJobSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2021 Kai Sassnowski
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/ksassnowski/venture
*/

namespace Sassnowski\Venture\Serializer;

use Laravel\SerializableClosure\Serializers\UnserializeException;

interface WorkflowJobSerializer
{
public function serialize(object $job): string;

/**
* @throws UnserializeException thrown if the string could not be unserialized for any reason
*/
public function unserialize(string $serializedJob): object;
}
9 changes: 8 additions & 1 deletion src/UnserializeJobExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@
namespace Sassnowski\Venture;

use Illuminate\Contracts\Queue\Job;
use Sassnowski\Venture\Serializer\WorkflowJobSerializer;
use function class_uses_recursive;

final class UnserializeJobExtractor implements JobExtractor
{
public function __construct(private WorkflowJobSerializer $serializer)
{
}

public function extractWorkflowJob(Job $queueJob): ?object
{
$instance = \unserialize($queueJob->payload()['data']['command']);
$instance = $this->serializer->unserialize(
$queueJob->payload()['data']['command'],
);

$uses = class_uses_recursive($instance);

Expand Down
14 changes: 14 additions & 0 deletions src/VentureServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

use Illuminate\Support\ServiceProvider;
use Sassnowski\Venture\Manager\WorkflowManager;
use Sassnowski\Venture\Serializer\Base64WorkflowSerializer;
use Sassnowski\Venture\Serializer\WorkflowJobSerializer;
use function config;

class VentureServiceProvider extends ServiceProvider
Expand Down Expand Up @@ -45,6 +47,7 @@ public function register(): void
$this->registerManager();
$this->registerJobExtractor();
$this->registerStepIdGenerator();
$this->registerJobSerializer();
}

private function registerManager(): void
Expand Down Expand Up @@ -73,4 +76,15 @@ private function registerStepIdGenerator(): void
),
);
}

private function registerJobSerializer(): void
{
$this->app->bind(
WorkflowJobSerializer::class,
config(
'venture.workflow_job_serializer_class',
Base64WorkflowSerializer::class,
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@

use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Str;
use Sassnowski\Venture\ClassNameStepIdGenerator;
use Sassnowski\Venture\JobExtractor;
use Sassnowski\Venture\Manager\WorkflowManager;
use Sassnowski\Venture\Serializer\Base64WorkflowSerializer;
use Sassnowski\Venture\Serializer\WorkflowJobSerializer;
use Sassnowski\Venture\StepIdGenerator;
use Sassnowski\Venture\UnserializeJobExtractor;
use Stubs\TestJob1;
use Stubs\TestJob2;
Expand All @@ -39,24 +43,30 @@
Bus::assertDispatched(TestJob2::class);
});

it('can handle missing workflow step id generator class in config', function (): void {
it('can handle missing class keys in config', function (string $abstract, string $defaultClass): void {
config([
'venture' => [
'workflow_table' => 'workflows',
'jobs_table' => 'workflow_jobs',
],
]);

expect(app('venture.manager'))->toBeInstanceOf(WorkflowManager::class);
});

it('can handle missing job extractor class in config', function (): void {
config([
'venture' => [
'workflow_table' => 'workflows',
'jobs_table' => 'workflow_jobs',
],
]);

expect(app(JobExtractor::class))->toBeInstanceOf(UnserializeJobExtractor::class);
});
expect(app($abstract))->toBeInstanceOf($defaultClass);
})->with([
'workflow manager' => [
'venture.manager',
WorkflowManager::class,
],
'job extractor' => [
JobExtractor::class,
UnserializeJobExtractor::class,
],
'workflow step id generator' => [
StepIdGenerator::class,
ClassNameStepIdGenerator::class,
],
'workflow serializer' => [
WorkflowJobSerializer::class,
Base64WorkflowSerializer::class,
],
]);
78 changes: 78 additions & 0 deletions tests/Base64SerializerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2021 Kai Sassnowski
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/ksassnowski/venture
*/

use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Laravel\SerializableClosure\Serializers\UnserializeException;
use Mockery as m;
use Sassnowski\Venture\Serializer\Base64WorkflowSerializer;
use Stubs\TestJob1;

it('simply serializes workflow jobs if a non-postgres connection is used', function (): void {
$connection = m::mock(MySqlConnection::class);
$serializer = new Base64WorkflowSerializer($connection);
$workflowJob = new TestJob1();

$result = $serializer->serialize($workflowJob);

expect($result)->toBe(\serialize($workflowJob));
});

it('base64 encodes workflow jobs if a postgres connection is used', function (): void {
$connection = m::mock(PostgresConnection::class);
$serializer = new Base64WorkflowSerializer($connection);
$workflowJob = new TestJob1();

$result = $serializer->serialize($workflowJob);

expect($result)->toBe(\base64_encode(\serialize($workflowJob)));
});

it('simply unserializes workflow jobs if a non-postgres connection is used', function (): void {
$connection = m::mock(MySqlConnection::class);
$serializer = new Base64WorkflowSerializer($connection);
$workflowJob = new TestJob1();
$serializedJob = \serialize($workflowJob);

$result = $serializer->unserialize($serializedJob);

expect($result)->toEqual($workflowJob);
});

it('unserializes and base64 decodes the job if a postgres connection is used', function (): void {
$connection = m::mock(PostgresConnection::class);
$serializer = new Base64WorkflowSerializer($connection);
$workflowJob = new TestJob1();
$serializedJob = \base64_encode(\serialize($workflowJob));

$result = $serializer->unserialize($serializedJob);

expect($result)->toEqual($workflowJob);
});

it('does not base64 decode a job if it has not been encoded before even if using a postgres connection', function (): void {
$connection = m::mock(PostgresConnection::class);
$serializer = new Base64WorkflowSerializer($connection);
$workflowJob = new TestJob1();
$serializedJob = \serialize($workflowJob);

$result = $serializer->unserialize($serializedJob);

expect($result)->toEqual($workflowJob);
});

it('throws an exception if the job could not be unserialized', function (): void {
$serializer = new Base64WorkflowSerializer(m::mock(ConnectionInterface::class));
$serializer->unserialize('::not-a-valid-serialized-string::');
})->throws(UnserializeException::class);
Loading