Skip to content

Commit

Permalink
[1.x] Extract insert all (#105)
Browse files Browse the repository at this point in the history
* Extracts `insertMany` method

* add tests

* Fix code styling

* Formatting

* reuse method logic

---------

Co-authored-by: timacdonald <[email protected]>
  • Loading branch information
timacdonald and timacdonald authored May 29, 2024
1 parent 892d91b commit 46c0714
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 15 deletions.
41 changes: 26 additions & 15 deletions src/Drivers/DatabaseDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,23 +168,17 @@ public function getAll($features): array

$inserts[] = [
'name' => $feature,
'scope' => Feature::serializeScope($scope),
'value' => json_encode($value, flags: JSON_THROW_ON_ERROR),
'scope' => $scope,
'value' => $value,
];

return $value;
});
})->all())->all();

if ($inserts->isNotEmpty()) {
$now = Carbon::now();

try {
$this->newQuery()->insert($inserts->map(fn ($insert) => [
...$insert,
static::CREATED_AT => $now,
static::UPDATED_AT => $now,
])->all());
$this->insertMany($inserts->all());
} catch (UniqueConstraintViolationException $e) {
if ($this->retryDepth === 2) {
throw new RuntimeException('Unable to insert feature values into the database.', previous: $e);
Expand Down Expand Up @@ -222,7 +216,7 @@ public function get($feature, $scope): mixed
$this->insert($feature, $scope, $value);
} catch (UniqueConstraintViolationException $e) {
if ($this->retryDepth === 1) {
throw new RuntimeException('Unable to insert feature value from the database.', previous: $e);
throw new RuntimeException('Unable to insert feature value into the database.', previous: $e);
}

$this->retryDepth++;
Expand Down Expand Up @@ -332,13 +326,30 @@ protected function update($feature, $scope, $value)
*/
protected function insert($feature, $scope, $value)
{
return $this->newQuery()->insert([
return $this->insertMany([[
'name' => $feature,
'scope' => Feature::serializeScope($scope),
'value' => json_encode($value, flags: JSON_THROW_ON_ERROR),
static::CREATED_AT => $now = Carbon::now(),
'scope' => $scope,
'value' => $value,
]]);
}

/**
* Insert the given feature values into storage.
*
* @param array<int, array{name: string, scope: mixed, value: mixed}> $inserts
* @return bool
*/
protected function insertMany($inserts)
{
$now = Carbon::now();

return $this->newQuery()->insert(array_map(fn ($insert) => [
'name' => $insert['name'],
'scope' => Feature::serializeScope($insert['scope']),
'value' => json_encode($insert['value'], flags: JSON_THROW_ON_ERROR),
static::CREATED_AT => $now,
static::UPDATED_AT => $now,
]);
], $inserts));
}

/**
Expand Down
89 changes: 89 additions & 0 deletions tests/Feature/DatabaseDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

namespace Tests\Feature;

use Exception;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Laravel\Pennant\Contracts\FeatureScopeable;
use Laravel\Pennant\Events\AllFeaturesPurged;
Expand All @@ -19,6 +23,7 @@
use Laravel\Pennant\Events\FeatureUpdatedForAllScopes;
use Laravel\Pennant\Events\UnknownFeatureResolved;
use Laravel\Pennant\Feature;
use RuntimeException;
use Tests\TestCase;
use Workbench\App\Models\User;
use Workbench\Database\Factories\UserFactory;
Expand Down Expand Up @@ -1293,6 +1298,90 @@ public function test_it_can_list_stored_features()

$this->assertSame(Feature::stored(), ['bar', 'baz']);
}

public function test_it_retries_3_times_and_then_fails()
{
Feature::define('foo', fn () => true);
Feature::define('bar', fn () => true);
$insertAttempts = 0;
DB::listen(function (QueryExecuted $event) use (&$insertAttempts) {
if (Str::startsWith($event->sql, 'insert into "features"')) {
$insertAttempts++;
DB::table('features')->delete();
throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException());
}
});

try {
Feature::for('tim')->loadMissing(['foo', 'bar']);
$this->fail('Should have failed.');
} catch (Exception $e) {
$this->assertInstanceOf(RuntimeException::class, $e);
$this->assertSame('Unable to insert feature values into the database.', $e->getMessage());
$this->assertInstanceOf(UniqueConstraintViolationException::class, $e->getPrevious());
}

$this->assertSame(3, $insertAttempts);
}

public function test_it_only_retries_on_conflicts()
{
Feature::define('foo', fn () => true);
Feature::define('bar', fn () => true);
$insertAttempts = 0;
DB::listen(function (QueryExecuted $event) use (&$insertAttempts) {
if (Str::startsWith($event->sql, 'insert into "features"')) {
$insertAttempts++;
throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException());
}
});

Feature::for('tim')->loadMissing(['foo', 'bar']);

$this->assertSame(1, $insertAttempts);
}

public function test_it_retries_2_times_and_then_fails_for_individual_queries()
{
Feature::define('foo', fn () => true);
Feature::define('bar', fn () => true);
$insertAttempts = 0;
DB::listen(function (QueryExecuted $event) use (&$insertAttempts) {
if (Str::startsWith($event->sql, 'insert into "features"')) {
$insertAttempts++;
DB::table('features')->delete();
throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException());
}
});

try {
Feature::driver('database')->get('foo', 'tim');
$this->fail('Should have failed.');
} catch (Exception $e) {
$this->assertInstanceOf(RuntimeException::class, $e);
$this->assertSame('Unable to insert feature value into the database.', $e->getMessage());
$this->assertInstanceOf(UniqueConstraintViolationException::class, $e->getPrevious());
}

$this->assertSame(2, $insertAttempts);
}

public function test_it_only_retries_on_conflicts_for_individual_queries()
{
Feature::define('foo', fn () => true);
Feature::define('bar', fn () => true);
$insertAttempts = 0;
DB::listen(function (QueryExecuted $event) use (&$insertAttempts) {
if (Str::startsWith($event->sql, 'insert into "features"')) {
$insertAttempts++;
throw new UniqueConstraintViolationException($event->connectionName, $event->sql, $event->bindings, new RuntimeException());
}
});

Feature::driver('database')->get('foo', 'tim');

$this->assertSame(1, $insertAttempts);
}
}

class UnregisteredFeature
Expand Down

0 comments on commit 46c0714

Please sign in to comment.