Skip to content

UniqueConstraintViolationException caused by parallel requests resolving feature values within a DB transaction #138

@kluu13

Description

@kluu13

Pennant Version

1.16.0

Laravel Version

11.44.2

PHP Version

8.3.17

Database Driver & Version

MySQL 8.0.41-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))

Description

There appears to be a rare race condition when parallel requests attempt to resolve a feature's value from storage (using the database driver), within a database transaction. This only happens when resolving for the first time (i.e. when the feature + value have yet to be stored). Note that this is also within an unauthenticated context.

I have not been able to reproduce this without wrapping it in a DB transaction.

(Illuminate\\Database\\UniqueConstraintViolationException(code: 23000): SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'test-feature-__laravel_null' for key 'features.features_name_scope_unique' (Connection: mysql, SQL: insert into `features` (`created_at`, `name`, `scope`, `updated_at`, `value`) values (2025-03-26 20:13:02, test-feature, __laravel_null, 2025-03-26 20:13:02, false)) at /home/myproject/code/vendor/laravel/framework/src/Illuminate/Database/Connection.php:820)

Steps To Reproduce

  1. Configure your Pennant store to use the database driver.
  2. Define a test feature in AppServiceProvider.php:
 /**
  * Bootstrap any application services.
  */
 public function boot(): void
 {
     Feature::define('test-feature', Lottery::odds(1 / 2));
 }
  1. Set up a test route like so:
Route::get('/test', function () {
    DB::transaction(function (): void {
        Feature::when(
            feature: 'test-feature',
            whenActive: fn () => Log::info('call external API 1'),
            whenInactive: fn () => Log::info('call external API 2')
        );
    });

    return response();
});
  1. Purge features from storage and send parallel requests:
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
use Laravel\Pennant\Feature;

// Truncate features table.
Feature::purge();

// Send requests in parallel.
$responses = Http::pool(
  fn (Pool $pool) => [
    $pool->get("http://myapp.test/test"),
    $pool->get("http://myapp.test/test")
  ]
);

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions