-
-
Notifications
You must be signed in to change notification settings - Fork 17
Add read-through filesystems for gradual storage migrations #620
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| use Aws\S3\S3Client; | ||
| use Closure; | ||
| use Google\Cloud\Storage\StorageClient as GcsClient; | ||
| use Hypervel\Context\CoroutineContext; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Declare (Based on your team's feedback about split-package runtime dependencies.) Prompt for AI agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added hypervel/context as a direct filesystem dependency in c78a17d. It was already installed transitively through hypervel/coroutine, so the standalone resolution failure did not occur, but the direct declaration is appropriate. |
||
| use Hypervel\Contracts\Container\Container; | ||
| use Hypervel\Contracts\Filesystem\Cloud; | ||
| use Hypervel\Contracts\Filesystem\Factory as FactoryContract; | ||
|
|
@@ -58,6 +59,11 @@ class FilesystemManager implements FactoryContract | |
| */ | ||
| protected const string ON_DEMAND_DISK_NAME = 'ondemand'; | ||
|
|
||
| /** | ||
| * The coroutine-local construction stack prefix for each manager. | ||
| */ | ||
| protected const string READ_THROUGH_CONTEXT_KEY_PREFIX = '__filesystem.read-through.construction.'; | ||
|
|
||
| /** | ||
| * Google Cloud Storage client constructor options supported by the installed SDK. | ||
| */ | ||
|
|
@@ -244,7 +250,7 @@ private function resolveConstructionDescriptor( | |
| return $this->createClientPooledDisk($driver, $config); | ||
| } | ||
|
|
||
| $driverMethod = 'create' . ucfirst($driver) . 'Driver'; | ||
| $driverMethod = 'create' . Str::studly($driver) . 'Driver'; | ||
|
|
||
| if (! method_exists($this, $driverMethod)) { | ||
| throw new InvalidArgumentException("Driver [{$driver}] is not supported."); | ||
|
|
@@ -434,6 +440,95 @@ public function createS3Driver(array $config): Cloud | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Create an instance of the read-through driver. | ||
| */ | ||
| public function createReadThroughDriver(array $config, string $name = 'read-through'): Filesystem | ||
| { | ||
| if (! isset($config['primary']) || $config['primary'] === '' || $config['primary'] === []) { | ||
| throw new InvalidArgumentException('Read-through disk is missing "primary" configuration option.'); | ||
| } | ||
| if (! isset($config['fallback']) || $config['fallback'] === '' || $config['fallback'] === []) { | ||
| throw new InvalidArgumentException('Read-through disk is missing "fallback" configuration option.'); | ||
| } | ||
| if ($config['primary'] === $config['fallback']) { | ||
| throw new InvalidArgumentException('Read-through disk requires distinct "primary" and "fallback" disks.'); | ||
| } | ||
|
|
||
| // Scoped inline sides can re-enter construction without resolving a named disk. | ||
| $contextKey = self::READ_THROUGH_CONTEXT_KEY_PREFIX . spl_object_id($this); | ||
| $stack = CoroutineContext::get($contextKey, []); | ||
| $label = $name === self::ON_DEMAND_DISK_NAME ? '(on-demand)' : $name; | ||
|
|
||
| foreach ($stack as $entry) { | ||
| if ($entry['config'] !== $config) { | ||
| continue; | ||
| } | ||
|
|
||
| if (count($stack) === 1) { | ||
| throw new InvalidArgumentException("Read-through disk [{$label}] cannot reference itself."); | ||
| } | ||
|
|
||
| $cycle = [...array_column($stack, 'name'), $label]; | ||
|
|
||
| throw new InvalidArgumentException('Circular read-through disk definition detected: ' . implode(' -> ', $cycle) . '.'); | ||
| } | ||
|
|
||
| CoroutineContext::set($contextKey, [...$stack, ['config' => $config, 'name' => $label]]); | ||
|
|
||
| try { | ||
| $primary = is_array($config['primary']) | ||
| ? $this->resolveWithLogicalName(self::ON_DEMAND_DISK_NAME, $config['primary'], null) | ||
| : $this->disk($config['primary']); | ||
| $fallback = is_array($config['fallback']) | ||
| ? $this->resolveWithLogicalName(self::ON_DEMAND_DISK_NAME, $config['fallback'], null) | ||
| : $this->disk($config['fallback']); | ||
|
|
||
| if (! $primary instanceof Cloud || ! $fallback instanceof Cloud) { | ||
| throw new InvalidArgumentException('Read-through disks must implement the cloud filesystem contract.'); | ||
| } | ||
|
|
||
| $adapter = new ReadThroughFilesystemAdapter( | ||
| $this->readThroughOperator($primary), | ||
| $this->readThroughOperator($fallback), | ||
| $config['throw_on_promotion_failure'] ?? false, | ||
| $config['copy'] ?? true, | ||
| ); | ||
|
|
||
| return new ReadThroughFilesystem( | ||
| $this->createFlysystem($adapter, $config), | ||
| $primary instanceof FilesystemAdapter ? $primary->getAdapter() : $adapter, | ||
| array_replace($primary->getConfig(), $config), // @phpstan-ignore method.notFound (Pooled decorators forward adapter accessors.) | ||
| $primary, | ||
| $fallback, | ||
| $config['prefix'] ?? '', | ||
| $adapter, | ||
| ); | ||
| } finally { | ||
| if ($stack === []) { | ||
| CoroutineContext::forget($contextKey); | ||
| } else { | ||
| CoroutineContext::set($contextKey, $stack); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get a side operator without exposing borrowed clients or losing native cloud reads. | ||
| */ | ||
| protected function readThroughOperator(Cloud $disk): FilesystemOperator | ||
| { | ||
| if ($disk instanceof AwsS3V3Adapter || $disk instanceof GoogleCloudStorageAdapter) { | ||
| return new FilesystemOperatorAdapter( | ||
| static fn (Closure $operation): mixed => $operation($disk->getDriver()), | ||
| $disk->readStreamRangeOrFail(...), | ||
| $disk->readStreamRangeOrFail(...), | ||
| ); | ||
| } | ||
|
|
||
| return $disk instanceof FilesystemAdapter ? $disk->getDriver() : $disk->getOperator(); // @phpstan-ignore method.notFound (Pooled decorators forward the borrow-safe accessor.) | ||
|
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Do not assume every Prompt for AI agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Documented the supported composition. Static configured scoped disks work through configuration resolution. A dynamic scoped proxy should wrap the read-through disk, rather than be an individual side; it deliberately rejects raw internal access to protect its prefix. Arbitrary Cloud implementations do not necessarily supply the additional Flysystem and adapter capabilities this feature requires. No unsafe raw accessor was added. |
||
| } | ||
|
|
||
| /** | ||
| * Derive the S3 client construction config from a disk config. | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.