From 0be3edf43a34827babbbc143986248b50ff898f7 Mon Sep 17 00:00:00 2001 From: Travis Austin Date: Wed, 16 Jul 2025 10:23:07 -0700 Subject: [PATCH 1/2] Added comments to workflow and activity classes --- src/Activity.php | 42 +++++++++++++++++++ src/ActivityStub.php | 37 +++++++++++++++- src/Middleware/ActivityMiddleware.php | 23 ++++++++++ .../WithoutOverlappingMiddleware.php | 20 +++++++++ src/Workflow.php | 17 ++++++++ src/WorkflowStub.php | 16 +++++++ 6 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/Activity.php b/src/Activity.php index 3f9d509..df459fa 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -25,6 +25,27 @@ use Workflow\Models\StoredWorkflow; use Workflow\Serializers\Serializer; +/** + * Class Activity - A dispatchable job that will be dispatched to a Laravel queue for a worker to process + * + * This is an abstract class the should be extended by your own activity classes. This base class represents a + * dispatchable job that will be dispatched to a Laravel queue for a worker to process. + * + * When instantiated and dispatched to a queue, this class receives the following properties: + * - $index: The index of the activity in the workflow. Each time a workflow yields to wait for an activity, child + * workflow, or side effect, this index is incremented. + * - $now: The "current" time in ISO 8601 format. The calling workflow tracks the time when the activity is + * dispatched and that time can be used in the activity logic. This allows retried activities to be retried using + * the same time value. + * - $storedWorkflow: The database model representing the workflow from which this activity was dispatched. + * - ...$arguments: The arguments passed to execute() method of the class that extends this class. + * + * Note: you should never instantiate this class or its children directly. Instead, you should call it from a workflow + * class using the `ActivityStub::make(YourChildActivityClass::class, ...$arguments)` method. + * + * + * @package Workflow + */ class Activity implements ShouldBeEncrypted, ShouldQueue { use Dispatchable; @@ -87,16 +108,25 @@ public function webhookUrl(string $signalMethod = ''): string public function handle() { + // If the child class does not implement the execute method, throw an exception if (! method_exists($this, 'execute')) { throw new BadMethodCallException('Execute method not implemented.'); } $this->container = App::make(Container::class); + // If this activity has already been executed and a return value has been stored in the database, then + // return. The middleware will handle dispatching its parent workflow to continue the execution of the + // workflow. if ($this->storedWorkflow->logs()->whereIndex($this->index)->exists()) { return; } + // Execute the child class's execute method, passing in the $...arguments that were passed to the + // ActivityStub::make() method. If the child class throws an exception, it will be caught here and + // the exception will be stored in the database. If the exception is a NonRetryableExceptionContract, + // then the workflow will be failed. If the exception is not a NonRetryableExceptionContract, then + // the exception will be rethrown, caught by the middleware, and the activity will be retried. try { return $this->{'execute'}(...$this->resolveClassMethodDependencies($this->arguments, $this, 'execute')); } catch (\Throwable $throwable) { @@ -117,20 +147,30 @@ public function handle() public function middleware() { return [ + // Ensure that this activity cannot run while the workflow is executing new WithoutOverlappingMiddleware( $this->storedWorkflow->id, WithoutOverlappingMiddleware::ACTIVITY, 0, $this->timeout ), + // Dispatch activity lifecycle events, execute the activity, and store the activity output in the database new ActivityMiddleware(), ]; } + /** + * This method is called when the activity throws an exception that is a NonRetryableExceptionContract or + * if the activity throws an exception that is not a NonRetryableExceptionContract but the activity is not + * retryable (i.e. it has been released back to the queue more than $tries times). + * + */ public function failed(Throwable $throwable): void { + // Instantiate the WorkflowStub class for the workflow from which this activity was dispatched $workflow = $this->storedWorkflow->toWorkflow(); + // Build a serializable version of the exception $file = new SplFileObject($throwable->getFile()); $iterator = new LimitIterator($file, max(0, $throwable->getLine() - 4), 7); @@ -146,6 +186,8 @@ public function failed(Throwable $throwable): void 'snippet' => array_slice(iterator_to_array($iterator), 0, 7), ]; + // Dispatch a job to store the exception in the database and to dispatch the parent workflow to continue + // the execution of the workflow. Exception::dispatch( $this->index, $this->now, diff --git a/src/ActivityStub.php b/src/ActivityStub.php index 3eb8cbd..fd87e01 100644 --- a/src/ActivityStub.php +++ b/src/ActivityStub.php @@ -12,26 +12,56 @@ use Throwable; use Workflow\Serializers\Serializer; +/** ActivityStub - A class called from within a Workflow to execute an Activity + * + * This class is called from within the execute() method of a Workflow class to execute an Activity. It has three + * method: + * - async: accept a callable ane execute it as if it were a defined child workflow + * - make: returns an unfilled promise if that activity has not been executed yet, otherwise returns a fulfilled + * promise with the result of the activity from when it was executed. + * - all: accepts an array of ActivityStub::make() promises and returns an array of unfilled/fulfilled promises. It is + * similar to ActivityStub::make() but it allows the system to execute multiple activities at the same time. + * + */ final class ActivityStub { + /** + * This method accepts an array of ActivityStub::make() promises and returns an array of unfilled/fulfilled promises. + * It is similar to ActivityStub::make() but it allows the system to execute multiple activities at the same time. + */ public static function all(iterable $promises): PromiseInterface { return all([...$promises]); } + /** + * This method accepts a callable and returns a promise that will execute that callable as if it were a defined + * child workflow. It is used to execute a callback function directly from within a Workflow without having to + * define a child workflow. + */ public static function async(callable $callback): PromiseInterface { return ChildWorkflowStub::make(AsyncWorkflow::class, new SerializableClosure($callback)); } + /** + * This method accepts the class name for an activity that extends the Activity class and the arguments to pass + * to that activity. When this is called from within a workflow for the first time, this will dispatch the + * activity to the queue and return an unfilled promise. The workflow will then exit and wait for the activity + * to complete. When this is called again (when the workflow is resumed), this will return a fulfilled promise + * with the result of the activity and the workflow will continue with the result. + */ public static function make($activity, ...$arguments): PromiseInterface { $context = WorkflowStub::getContext(); + // Query the database to see if the activity has already been executed $log = $context->storedWorkflow->logs() ->whereIndex($context->index) ->first(); + // If we are running unit tests, and we have a mock for this activity, then + // use the mock instead of dispatching the activity. if (WorkflowStub::faked()) { $mocks = WorkflowStub::mocks(); @@ -52,7 +82,9 @@ public static function make($activity, ...$arguments): PromiseInterface } } - if ($log) { + // If the activity has already been executed and the result was available in the database, then + // return a fulfilled promise with that result. + if ($log) { ++$context->index; WorkflowStub::setContext($context); $result = Serializer::unserialize($log->result); @@ -66,6 +98,9 @@ public static function make($activity, ...$arguments): PromiseInterface return resolve($result); } + // At this point, we know that the activity has not yet been executed. Dispatch it to the queue and + // return an unfilled promise to signal to the workflow that it can exit and wait for the activity to + // complete. $activity::dispatch($context->index, $context->now, $context->storedWorkflow, ...$arguments); ++$context->index; diff --git a/src/Middleware/ActivityMiddleware.php b/src/Middleware/ActivityMiddleware.php index 3f6ea4f..a708711 100644 --- a/src/Middleware/ActivityMiddleware.php +++ b/src/Middleware/ActivityMiddleware.php @@ -11,6 +11,29 @@ use Workflow\Events\ActivityFailed; use Workflow\Events\ActivityStarted; +/** + * Class ActivityMiddleware + * + * This middleware orchestrates the complete lifecycle of activity execution within a workflow. + * + * Execution Flow: + * 1. Dispatch ActivityStarted event with activity details and unique UUID + * 2. Allow the activity to execute ($next($job)) + * 3. Store the activity output/result in the database + * 4. Attempt to update the workflow status to "pending" in preparation for continuation + * 5. If status transition is valid: Dispatch the parent workflow back to the queue to continue execution + * 6. If status transition fails (workflow already "running"): Release this activity back to the queue for retry + * 7. Dispatch ActivityCompleted event + * + * Important: Due to the state transition logic in step 4-6, activities may be completed more than once + * if there are timing conflicts with workflow state changes. + * + * On failure, it captures detailed exception information (including code snippets) and dispatches + * ActivityFailed event before re-throwing the exception. + * + * This middleware acts as the bridge that allows activities to seamlessly hand their results back + * to their parent workflow for continued execution, while managing state transitions safely. + */ final class ActivityMiddleware { public function handle($job, $next): void diff --git a/src/Middleware/WithoutOverlappingMiddleware.php b/src/Middleware/WithoutOverlappingMiddleware.php index 2ab93f1..71bb8cc 100644 --- a/src/Middleware/WithoutOverlappingMiddleware.php +++ b/src/Middleware/WithoutOverlappingMiddleware.php @@ -9,6 +9,26 @@ use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; +/** + * Class WithoutOverlappingMiddleware + * + * This middleware ensures mutual exclusion between workflow execution and activity execution for a specific + * workflow instance using a semaphore-based locking system. + * + * Key Behaviors: + * - **Workflow Exclusivity**: Only 1 instance of a workflow can run at a time + * - **Workflow-Activity Mutual Exclusion**: When a workflow is executing, none of its activities can run simultaneously + * - **Activity Concurrency**: When the workflow is NOT executing, multiple activities from that workflow can run simultaneously + * - **Per-Workflow Isolation**: This applies per workflow instance (identified by workflowId), not globally + * + * In simple terms: when a workflow is executing, nothing else for that workflow is running. If the workflow is not + * being executed, then children of the workflow can run freely. + * + * This design prevents race conditions while maximizing concurrency - the workflow logic (the "conductor") has + * exclusive control when making decisions, but when it's waiting for work to be done, multiple activities + * (the "workers") can execute in parallel without interfering with each other. + * + */ class WithoutOverlappingMiddleware { use InteractsWithTime; diff --git a/src/Workflow.php b/src/Workflow.php index 636ffb7..23dbbc1 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -27,6 +27,19 @@ use Workflow\States\WorkflowWaitingStatus; use Workflow\Traits\Sagas; +/** + * Workflow - A dispatchable job + * + * This is an abstract class the should be extended by your own workflow classes. This base class represents a + * dispatchable job that will be dispatched to a Laravel queue for a worker to process. + * + * When instantiated and dispatched to a queue, this class receives the following properties: + * - $storedWorkflow: The database model representing this workflow. + * - ...$arguments: The arguments that will be passed to execute() method of the class that extends this class. + * + * Note: you should never instantiate this class or its children directly. Instead, you should instantiate it using the + * `WorkflowStub::make(YourChildWorkflowClass::class)->start($..$arguments)`. + */ class Workflow implements ShouldBeEncrypted, ShouldQueue { use Dispatchable; @@ -78,6 +91,8 @@ public function query($method) public function middleware() { + // This middleware is used to prevent multiple instances of the same workflow from running at the same time. + // @see WithoutOverlappingMiddleware for details on its implementation. $parentWorkflow = $this->storedWorkflow->parents() ->first(); @@ -93,6 +108,8 @@ public function middleware() public function failed(Throwable $throwable): void { + // If an activity is dispatched from a workflow and it fails, the workflow will receive the exception. If the + // workflow does not handle the exception, it will be caught here and the workflow will be marked as failed. try { $this->storedWorkflow->toWorkflow() ->fail($throwable); diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index 9e61c5e..a4f997b 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -217,6 +217,13 @@ public function startAsChild(StoredWorkflow $parentWorkflow, int $index, $now, . $this->start(...$arguments); } + /** + * This method is called when a defined workflow job fails (i.e. the workflow does not elegantly handle an exception + * thrown by an activity). + * + * This method will store the exception in the database and transition the workflow to the failed state. It will then + * dispatch the WorkflowFailed event and call the fail method on any parent workflows. + */ public function fail($exception): void { $this->storedWorkflow->exceptions() @@ -252,6 +259,10 @@ public function fail($exception): void }); } + /** + * This method is called from an activity or child workflow with it completes. This method stores the result in the + * database and dispatches the workflow back to the queue so that it can be continued. + */ public function next($index, $now, $class, $result): void { try { @@ -269,6 +280,11 @@ public function next($index, $now, $class, $result): void $this->dispatch(); } + /** + * This method is called when a workflow is started and each time that an activity or child workflow completes. + * This method will update the status field for this workflow in the database to "pending", and will then + * dispatch the workflow to the queue so that it can be continued. + */ private function dispatch(): void { if ($this->created()) { From 14563f9caf208868d8ada1089282cfb5e722f8ea Mon Sep 17 00:00:00 2001 From: Travis Austin Date: Thu, 17 Jul 2025 09:25:32 -0700 Subject: [PATCH 2/2] Fixed indentations --- src/Activity.php | 44 ++++++++-------- src/ActivityStub.php | 51 +++++++++---------- .../WithoutOverlappingMiddleware.php | 1 - src/Workflow.php | 8 +-- src/WorkflowStub.php | 32 ++++++------ 5 files changed, 65 insertions(+), 71 deletions(-) diff --git a/src/Activity.php b/src/Activity.php index df459fa..a4146df 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -42,9 +42,6 @@ * * Note: you should never instantiate this class or its children directly. Instead, you should call it from a workflow * class using the `ActivityStub::make(YourChildActivityClass::class, ...$arguments)` method. - * - * - * @package Workflow */ class Activity implements ShouldBeEncrypted, ShouldQueue { @@ -108,25 +105,25 @@ public function webhookUrl(string $signalMethod = ''): string public function handle() { - // If the child class does not implement the execute method, throw an exception + // If the child class does not implement the execute method, throw an exception if (! method_exists($this, 'execute')) { throw new BadMethodCallException('Execute method not implemented.'); } $this->container = App::make(Container::class); - // If this activity has already been executed and a return value has been stored in the database, then - // return. The middleware will handle dispatching its parent workflow to continue the execution of the - // workflow. + // If this activity has already been executed and a return value has been stored in the database, then + // return. The middleware will handle dispatching its parent workflow to continue the execution of the + // workflow. if ($this->storedWorkflow->logs()->whereIndex($this->index)->exists()) { return; } - // Execute the child class's execute method, passing in the $...arguments that were passed to the - // ActivityStub::make() method. If the child class throws an exception, it will be caught here and - // the exception will be stored in the database. If the exception is a NonRetryableExceptionContract, - // then the workflow will be failed. If the exception is not a NonRetryableExceptionContract, then - // the exception will be rethrown, caught by the middleware, and the activity will be retried. + // Execute the child class's execute method, passing in the $...arguments that were passed to the + // ActivityStub::make() method. If the child class throws an exception, it will be caught here and + // the exception will be stored in the database. If the exception is a NonRetryableExceptionContract, + // then the workflow will be failed. If the exception is not a NonRetryableExceptionContract, then + // the exception will be rethrown, caught by the middleware, and the activity will be retried. try { return $this->{'execute'}(...$this->resolveClassMethodDependencies($this->arguments, $this, 'execute')); } catch (\Throwable $throwable) { @@ -147,30 +144,29 @@ public function handle() public function middleware() { return [ - // Ensure that this activity cannot run while the workflow is executing + // Ensure that this activity cannot run while the workflow is executing new WithoutOverlappingMiddleware( $this->storedWorkflow->id, WithoutOverlappingMiddleware::ACTIVITY, 0, $this->timeout ), - // Dispatch activity lifecycle events, execute the activity, and store the activity output in the database + // Dispatch activity lifecycle events, execute the activity, and store the activity output in the database new ActivityMiddleware(), ]; } - /** - * This method is called when the activity throws an exception that is a NonRetryableExceptionContract or - * if the activity throws an exception that is not a NonRetryableExceptionContract but the activity is not - * retryable (i.e. it has been released back to the queue more than $tries times). - * - */ + /** + * This method is called when the activity throws an exception that is a NonRetryableExceptionContract or + * if the activity throws an exception that is not a NonRetryableExceptionContract but the activity is not + * retryable (i.e. it has been released back to the queue more than $tries times). + */ public function failed(Throwable $throwable): void { - // Instantiate the WorkflowStub class for the workflow from which this activity was dispatched + // Instantiate the WorkflowStub class for the workflow from which this activity was dispatched $workflow = $this->storedWorkflow->toWorkflow(); - // Build a serializable version of the exception + // Build a serializable version of the exception $file = new SplFileObject($throwable->getFile()); $iterator = new LimitIterator($file, max(0, $throwable->getLine() - 4), 7); @@ -186,8 +182,8 @@ public function failed(Throwable $throwable): void 'snippet' => array_slice(iterator_to_array($iterator), 0, 7), ]; - // Dispatch a job to store the exception in the database and to dispatch the parent workflow to continue - // the execution of the workflow. + // Dispatch a job to store the exception in the database and to dispatch the parent workflow to continue + // the execution of the workflow. Exception::dispatch( $this->index, $this->now, diff --git a/src/ActivityStub.php b/src/ActivityStub.php index fd87e01..3b17b64 100644 --- a/src/ActivityStub.php +++ b/src/ActivityStub.php @@ -21,47 +21,46 @@ * promise with the result of the activity from when it was executed. * - all: accepts an array of ActivityStub::make() promises and returns an array of unfilled/fulfilled promises. It is * similar to ActivityStub::make() but it allows the system to execute multiple activities at the same time. - * */ final class ActivityStub { - /** - * This method accepts an array of ActivityStub::make() promises and returns an array of unfilled/fulfilled promises. - * It is similar to ActivityStub::make() but it allows the system to execute multiple activities at the same time. - */ + /** + * This method accepts an array of ActivityStub::make() promises and returns an array of unfilled/fulfilled promises. + * It is similar to ActivityStub::make() but it allows the system to execute multiple activities at the same time. + */ public static function all(iterable $promises): PromiseInterface { return all([...$promises]); } - /** - * This method accepts a callable and returns a promise that will execute that callable as if it were a defined - * child workflow. It is used to execute a callback function directly from within a Workflow without having to - * define a child workflow. - */ + /** + * This method accepts a callable and returns a promise that will execute that callable as if it were a defined + * child workflow. It is used to execute a callback function directly from within a Workflow without having to + * define a child workflow. + */ public static function async(callable $callback): PromiseInterface { return ChildWorkflowStub::make(AsyncWorkflow::class, new SerializableClosure($callback)); } - /** - * This method accepts the class name for an activity that extends the Activity class and the arguments to pass - * to that activity. When this is called from within a workflow for the first time, this will dispatch the - * activity to the queue and return an unfilled promise. The workflow will then exit and wait for the activity - * to complete. When this is called again (when the workflow is resumed), this will return a fulfilled promise - * with the result of the activity and the workflow will continue with the result. - */ + /** + * This method accepts the class name for an activity that extends the Activity class and the arguments to pass + * to that activity. When this is called from within a workflow for the first time, this will dispatch the + * activity to the queue and return an unfilled promise. The workflow will then exit and wait for the activity + * to complete. When this is called again (when the workflow is resumed), this will return a fulfilled promise + * with the result of the activity and the workflow will continue with the result. + */ public static function make($activity, ...$arguments): PromiseInterface { $context = WorkflowStub::getContext(); - // Query the database to see if the activity has already been executed + // Query the database to see if the activity has already been executed $log = $context->storedWorkflow->logs() ->whereIndex($context->index) ->first(); - // If we are running unit tests, and we have a mock for this activity, then - // use the mock instead of dispatching the activity. + // If we are running unit tests, and we have a mock for this activity, then + // use the mock instead of dispatching the activity. if (WorkflowStub::faked()) { $mocks = WorkflowStub::mocks(); @@ -82,9 +81,9 @@ public static function make($activity, ...$arguments): PromiseInterface } } - // If the activity has already been executed and the result was available in the database, then - // return a fulfilled promise with that result. - if ($log) { + // If the activity has already been executed and the result was available in the database, then + // return a fulfilled promise with that result. + if ($log) { ++$context->index; WorkflowStub::setContext($context); $result = Serializer::unserialize($log->result); @@ -98,9 +97,9 @@ public static function make($activity, ...$arguments): PromiseInterface return resolve($result); } - // At this point, we know that the activity has not yet been executed. Dispatch it to the queue and - // return an unfilled promise to signal to the workflow that it can exit and wait for the activity to - // complete. + // At this point, we know that the activity has not yet been executed. Dispatch it to the queue and + // return an unfilled promise to signal to the workflow that it can exit and wait for the activity to + // complete. $activity::dispatch($context->index, $context->now, $context->storedWorkflow, ...$arguments); ++$context->index; diff --git a/src/Middleware/WithoutOverlappingMiddleware.php b/src/Middleware/WithoutOverlappingMiddleware.php index 71bb8cc..b4002b1 100644 --- a/src/Middleware/WithoutOverlappingMiddleware.php +++ b/src/Middleware/WithoutOverlappingMiddleware.php @@ -27,7 +27,6 @@ * This design prevents race conditions while maximizing concurrency - the workflow logic (the "conductor") has * exclusive control when making decisions, but when it's waiting for work to be done, multiple activities * (the "workers") can execute in parallel without interfering with each other. - * */ class WithoutOverlappingMiddleware { diff --git a/src/Workflow.php b/src/Workflow.php index 23dbbc1..62dad54 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -91,8 +91,8 @@ public function query($method) public function middleware() { - // This middleware is used to prevent multiple instances of the same workflow from running at the same time. - // @see WithoutOverlappingMiddleware for details on its implementation. + // This middleware is used to prevent multiple instances of the same workflow from running at the same time. + // @see WithoutOverlappingMiddleware for details on its implementation. $parentWorkflow = $this->storedWorkflow->parents() ->first(); @@ -108,8 +108,8 @@ public function middleware() public function failed(Throwable $throwable): void { - // If an activity is dispatched from a workflow and it fails, the workflow will receive the exception. If the - // workflow does not handle the exception, it will be caught here and the workflow will be marked as failed. + // If an activity is dispatched from a workflow and it fails, the workflow will receive the exception. If the + // workflow does not handle the exception, it will be caught here and the workflow will be marked as failed. try { $this->storedWorkflow->toWorkflow() ->fail($throwable); diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index a4f997b..0e525a5 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -217,13 +217,13 @@ public function startAsChild(StoredWorkflow $parentWorkflow, int $index, $now, . $this->start(...$arguments); } - /** - * This method is called when a defined workflow job fails (i.e. the workflow does not elegantly handle an exception - * thrown by an activity). - * - * This method will store the exception in the database and transition the workflow to the failed state. It will then - * dispatch the WorkflowFailed event and call the fail method on any parent workflows. - */ + /** + * This method is called when a defined workflow job fails (i.e. the workflow does not elegantly handle an exception + * thrown by an activity). + * + * This method will store the exception in the database and transition the workflow to the failed state. It will then + * dispatch the WorkflowFailed event and call the fail method on any parent workflows. + */ public function fail($exception): void { $this->storedWorkflow->exceptions() @@ -259,10 +259,10 @@ public function fail($exception): void }); } - /** - * This method is called from an activity or child workflow with it completes. This method stores the result in the - * database and dispatches the workflow back to the queue so that it can be continued. - */ + /** + * This method is called from an activity or child workflow with it completes. This method stores the result in the + * database and dispatches the workflow back to the queue so that it can be continued. + */ public function next($index, $now, $class, $result): void { try { @@ -280,11 +280,11 @@ public function next($index, $now, $class, $result): void $this->dispatch(); } - /** - * This method is called when a workflow is started and each time that an activity or child workflow completes. - * This method will update the status field for this workflow in the database to "pending", and will then - * dispatch the workflow to the queue so that it can be continued. - */ + /** + * This method is called when a workflow is started and each time that an activity or child workflow completes. + * This method will update the status field for this workflow in the database to "pending", and will then + * dispatch the workflow to the queue so that it can be continued. + */ private function dispatch(): void { if ($this->created()) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy