Skip to content
Merged
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
44 changes: 36 additions & 8 deletions src/PermissionRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class PermissionRegistrar

private array $wildcardPermissionsIndex = [];

private bool $isLoadingPermissions = false;

/**
* PermissionRegistrar constructor.
*/
Expand Down Expand Up @@ -172,6 +174,7 @@ public function clearPermissionsCollection(): void
{
$this->permissions = null;
$this->wildcardPermissionsIndex = [];
$this->isLoadingPermissions = false;
}

/**
Expand All @@ -187,24 +190,49 @@ public function clearClassPermissions()
/**
* Load permissions from cache
* And turns permissions array into a \Illuminate\Database\Eloquent\Collection
*
* Thread-safe implementation to prevent race conditions in concurrent environments
* (e.g., Laravel Octane, Swoole, parallel requests)
*/
private function loadPermissions(): void
private function loadPermissions(int $retries = 0): void
{
// First check (without lock) - fast path for already loaded permissions
if ($this->permissions) {
return;
}

$this->permissions = $this->cache->remember(
$this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache()
);
// Prevent concurrent loading using a flag-based lock
// This protects against cache stampede and duplicate database queries
if ($this->isLoadingPermissions && $retries < 10) {
// Another thread is loading, wait and retry
usleep(10000); // Wait 10ms
$retries++;

// After wait, recursively check again if permissions were loaded
$this->loadPermissions($retries);

$this->alias = $this->permissions['alias'];
return;
}

$this->hydrateRolesCache();
// Set loading flag to prevent concurrent loads
$this->isLoadingPermissions = true;

$this->permissions = $this->getHydratedPermissionCollection();
try {
$this->permissions = $this->cache->remember(
$this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache()
);

$this->cachedRoles = $this->alias = $this->except = [];
$this->alias = $this->permissions['alias'];

$this->hydrateRolesCache();

$this->permissions = $this->getHydratedPermissionCollection();

$this->cachedRoles = $this->alias = $this->except = [];
} finally {
// Always release the loading flag, even if an exception occurs
$this->isLoadingPermissions = false;
}
}

/**
Expand Down