Skip to content

Commit 8e09fb6

Browse files
authored
Merge pull request #12 from rcalicdan/qualityoflifeimprovement
Qualityoflifeimprovement
2 parents 6223f09 + 2a845ba commit 8e09fb6

17 files changed

+152
-450
lines changed

src/Api/Async.php

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -116,36 +116,4 @@ public static function await(PromiseInterface $promise): mixed
116116
{
117117
return self::getAsyncOperations()->await($promise);
118118
}
119-
120-
/**
121-
* Create a safe async function with automatic error handling.
122-
*
123-
* The returned function will catch any exceptions thrown during execution
124-
* and convert them to rejected promises, preventing uncaught exceptions
125-
* from crashing the event loop. This is essential for building robust
126-
* async applications that can gracefully handle errors.
127-
*
128-
* @param callable $asyncFunction The async function to make safe
129-
* @return callable A safe version that always returns a promise
130-
*/
131-
public static function tryAsync(callable $asyncFunction): callable
132-
{
133-
return self::getAsyncOperations()->tryAsync($asyncFunction);
134-
}
135-
136-
/**
137-
* Convert a synchronous function to work in async contexts.
138-
*
139-
* Wraps a synchronous function so it can be used alongside async operations
140-
* without blocking the event loop. The function will be executed in a way
141-
* that doesn't interfere with concurrent async operations, making it safe
142-
* to use within fiber-based async workflows.
143-
*
144-
* @param callable $syncFunction The synchronous function to wrap
145-
* @return callable An async-compatible version of the function
146-
*/
147-
public static function asyncify(callable $syncFunction): callable
148-
{
149-
return self::getAsyncOperations()->asyncify($syncFunction);
150-
}
151119
}

src/Api/AsyncLoop.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,4 @@ public static function asyncSleep(float $seconds): void
158158
{
159159
self::getLoopOperations()->asyncSleep($seconds);
160160
}
161-
162-
/**
163-
* Run an async operation and measure its performance metrics.
164-
*
165-
* @param callable|PromiseInterface $asyncOperation The operation to benchmark
166-
* @return array Array containing 'result' and 'benchmark' keys with performance data
167-
*/
168-
public static function benchmark(callable|PromiseInterface $asyncOperation): array
169-
{
170-
return self::getLoopOperations()->benchmark($asyncOperation);
171-
}
172161
}

src/Api/Promise.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,17 @@ public static function timeout(callable|PromiseInterface|array $promises, float
142142
}
143143

144144
/**
145-
* Execute multiple tasks concurrently with a concurrency limit.
145+
* Execute multiple tasks concurrently with a specified concurrency limit.
146146
*
147-
* Processes an array of tasks (callables or promises) in controlled batches
148-
* to avoid overwhelming the system resources. This is essential for handling
149-
* large numbers of concurrent operations without exhausting memory, file
150-
* descriptors, or network connections. Unlike Promise::all(), this method
151-
* provides backpressure control.
147+
* IMPORTANT: For proper concurrency control, tasks should be callables that return
148+
* Promises, not pre-created Promise instances. Pre-created Promises are already
149+
* running and cannot be subject to concurrency limiting.
152150
*
153-
* @param array $tasks Array of tasks (callables or promises) to execute
154-
* @param int $concurrency Maximum number of concurrent executions (default: 10)
155-
* @return PromiseInterface A promise that resolves with all task results
151+
* @param array $tasks Array of callables that return Promises, or Promise instances
152+
* Note: Promise instances will be awaited but cannot be truly
153+
* limited since they're already running
154+
* @param int $concurrency Maximum number of tasks to run simultaneously
155+
* @return PromiseInterface Promise that resolves with an array of all results
156156
*/
157157
public static function concurrent(array $tasks, int $concurrency = 10): PromiseInterface
158158
{
@@ -167,7 +167,9 @@ public static function concurrent(array $tasks, int $concurrency = 10): PromiseI
167167
* processing large datasets or performing operations that require
168168
* significant resources without overwhelming the system.
169169
*
170-
* @param array $tasks Array of tasks (callables or promises) to execute
170+
* @param array $tasks Array of callables that return Promises, or Promise instances
171+
* Note: Promise instances will be awaited but cannot be truly
172+
* limited since they're already running
171173
* @param int $batchSize Size of each batch to process concurrently (default: 10)
172174
* @param int $concurrency Maximum number of concurrent executions (default: 10)
173175
* @return PromiseInterface A promise that resolves with all task results

src/Async/AsyncOperations.php

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -125,34 +125,6 @@ public function async(callable $asyncFunction): callable
125125
return $this->executionHandler->async($asyncFunction);
126126
}
127127

128-
/**
129-
* Convert a synchronous function to work in async contexts.
130-
*
131-
* This wraps a sync function so it can be used alongside async
132-
* operations without blocking the event loop.
133-
*
134-
* @param callable $syncFunction The synchronous function to wrap
135-
* @return callable An async-compatible version of the function
136-
*/
137-
public function asyncify(callable $syncFunction): callable
138-
{
139-
return $this->executionHandler->asyncify($syncFunction);
140-
}
141-
142-
/**
143-
* Create a safe async function with error handling.
144-
*
145-
* The returned function will catch exceptions and convert them
146-
* to rejected promises, preventing uncaught exceptions.
147-
*
148-
* @param callable $asyncFunction The async function to make safe
149-
* @return callable A safe version of the async function
150-
*/
151-
public function tryAsync(callable $asyncFunction): callable
152-
{
153-
return $this->executionHandler->tryAsync($asyncFunction, $this->contextHandler, $this->awaitHandler);
154-
}
155-
156128
/**
157129
* Await a promise and return its resolved value.
158130
*

src/Async/Handlers/AsyncExecutionHandler.php

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -42,54 +42,4 @@ public function async(callable $asyncFunction): callable
4242
});
4343
};
4444
}
45-
46-
/**
47-
* Convert a synchronous function into an asynchronous version.
48-
*
49-
* This is an alias for async() but with a name that emphasizes the
50-
* conversion from synchronous to asynchronous execution.
51-
*
52-
* @param callable $syncFunction The synchronous function to convert
53-
* @return callable A function that returns a Promise when called
54-
*/
55-
public function asyncify(callable $syncFunction): callable
56-
{
57-
return function (...$args) use ($syncFunction) {
58-
return new Promise(function ($resolve, $reject) use ($syncFunction, $args) {
59-
$fiber = new Fiber(function () use ($syncFunction, $args, $resolve, $reject) {
60-
try {
61-
$result = $syncFunction(...$args);
62-
$resolve($result);
63-
} catch (Throwable $e) {
64-
$reject($e);
65-
}
66-
});
67-
68-
EventLoop::getInstance()->addFiber($fiber);
69-
});
70-
};
71-
}
72-
73-
/**
74-
* Create an async function that automatically awaits Promise results.
75-
*
76-
* This creates an async function that will automatically await any Promise
77-
* returned by the wrapped function, providing a more convenient API for
78-
* chaining async operations.
79-
*
80-
* @param callable $asyncFunction The async function to wrap
81-
* @param FiberContextHandler $contextHandler Handler for fiber context validation
82-
* @param AwaitHandler $awaitHandler Handler for awaiting Promise results
83-
* @return callable A function that automatically awaits results
84-
*/
85-
public function tryAsync(callable $asyncFunction, FiberContextHandler $contextHandler, AwaitHandler $awaitHandler): callable
86-
{
87-
return $this->async(function (...$args) use ($asyncFunction, $awaitHandler) {
88-
try {
89-
return $awaitHandler->await($asyncFunction(...$args));
90-
} catch (Throwable $e) {
91-
throw $e; // Re-throw to be caught by calling code
92-
}
93-
});
94-
}
9545
}

src/Async/Handlers/ConcurrencyHandler.php

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,24 @@
1818
final readonly class ConcurrencyHandler
1919
{
2020
private AsyncExecutionHandler $executionHandler;
21+
private AwaitHandler $awaitHandler;
2122

2223
/**
2324
* @param AsyncExecutionHandler $executionHandler Handler for async execution
2425
*/
2526
public function __construct(AsyncExecutionHandler $executionHandler)
2627
{
2728
$this->executionHandler = $executionHandler;
29+
$this->awaitHandler = new AwaitHandler(new FiberContextHandler);
2830
}
2931

3032
/**
3133
* Execute multiple tasks concurrently with a specified concurrency limit.
3234
*
3335
* This method runs multiple tasks simultaneously while ensuring that no more
3436
* than the specified number of tasks run at the same time. Tasks can be either
35-
* callable functions or existing Promise instances.
37+
* callable functions or existing Promise instances. Promise instances will be
38+
* automatically wrapped to ensure proper concurrency control.
3639
*
3740
* @param array $tasks Array of callable tasks or Promise instances
3841
* @param int $concurrency Maximum number of tasks to run simultaneously (default: 10)
@@ -45,29 +48,33 @@ public function concurrent(array $tasks, int $concurrency = 10): PromiseInterfac
4548
return new Promise(function ($resolve, $reject) use ($tasks, $concurrency) {
4649
if ($concurrency <= 0) {
4750
$reject(new \InvalidArgumentException('Concurrency limit must be greater than 0'));
48-
4951
return;
5052
}
5153

5254
if (empty($tasks)) {
5355
$resolve([]);
54-
5556
return;
5657
}
5758

5859
// Convert tasks to indexed array and preserve original keys
5960
$taskList = array_values($tasks);
6061
$originalKeys = array_keys($tasks);
6162

63+
// Process tasks to ensure proper async wrapping
64+
$processedTasks = [];
65+
foreach ($taskList as $index => $task) {
66+
$processedTasks[$index] = $this->wrapTaskForConcurrency($task);
67+
}
68+
6269
$results = [];
6370
$running = 0;
6471
$completed = 0;
65-
$total = count($taskList);
72+
$total = count($processedTasks);
6673
$taskIndex = 0;
6774

6875
$processNext = function () use (
6976
&$processNext,
70-
&$taskList,
77+
&$processedTasks,
7178
&$originalKeys,
7279
&$running,
7380
&$completed,
@@ -81,22 +88,19 @@ public function concurrent(array $tasks, int $concurrency = 10): PromiseInterfac
8188
// Start as many tasks as we can up to the concurrency limit
8289
while ($running < $concurrency && $taskIndex < $total) {
8390
$currentIndex = $taskIndex++;
84-
$task = $taskList[$currentIndex];
91+
$task = $processedTasks[$currentIndex];
8592
$originalKey = $originalKeys[$currentIndex];
8693
$running++;
8794

8895
try {
89-
$promise = is_callable($task)
90-
? $this->executionHandler->async($task)()
91-
: $task;
96+
$promise = $this->executionHandler->async($task)();
9297

9398
if (! ($promise instanceof PromiseInterface)) {
9499
throw new RuntimeException('Task must return a Promise or be a callable that returns a Promise');
95100
}
96101
} catch (Throwable $e) {
97102
$running--;
98103
$reject($e);
99-
100104
return;
101105
}
102106

@@ -124,8 +128,7 @@ public function concurrent(array $tasks, int $concurrency = 10): PromiseInterfac
124128
->catch(function ($error) use (&$running, $reject) {
125129
$running--;
126130
$reject($error);
127-
})
128-
;
131+
});
129132
}
130133
};
131134

@@ -139,7 +142,8 @@ public function concurrent(array $tasks, int $concurrency = 10): PromiseInterfac
139142
*
140143
* This method processes tasks in batches sequentially, where each batch
141144
* runs tasks concurrently up to the specified limit, but waits for the
142-
* entire batch to complete before starting the next batch.
145+
* entire batch to complete before starting the next batch. Promise instances
146+
* will be automatically wrapped to ensure proper concurrency control.
143147
*
144148
* @param array $tasks Array of callable tasks or Promise instances
145149
* @param int $batchSize Number of tasks per batch (default: 10)
@@ -151,22 +155,27 @@ public function batch(array $tasks, int $batchSize = 10, ?int $concurrency = nul
151155
return new Promise(function ($resolve, $reject) use ($tasks, $batchSize, $concurrency) {
152156
if ($batchSize <= 0) {
153157
$reject(new \InvalidArgumentException('Batch size must be greater than 0'));
154-
155158
return;
156159
}
157160

158161
if (empty($tasks)) {
159162
$resolve([]);
160-
161163
return;
162164
}
163165

164166
$concurrency = $concurrency ?? $batchSize;
165167

166-
// Preserve original keys
168+
// Preserve original keys and wrap tasks
167169
$originalKeys = array_keys($tasks);
168170
$taskValues = array_values($tasks);
169-
$batches = array_chunk($taskValues, $batchSize, false);
171+
172+
// Process tasks to ensure proper async wrapping
173+
$processedTasks = [];
174+
foreach ($taskValues as $index => $task) {
175+
$processedTasks[$index] = $this->wrapTaskForConcurrency($task);
176+
}
177+
178+
$batches = array_chunk($processedTasks, $batchSize, false);
170179
$keyBatches = array_chunk($originalKeys, $batchSize, false);
171180

172181
$allResults = [];
@@ -186,7 +195,6 @@ public function batch(array $tasks, int $batchSize = 10, ?int $concurrency = nul
186195
) {
187196
if ($batchIndex >= $totalBatches) {
188197
$resolve($allResults);
189-
190198
return;
191199
}
192200

@@ -205,11 +213,44 @@ public function batch(array $tasks, int $batchSize = 10, ?int $concurrency = nul
205213
$batchIndex++;
206214
EventLoop::getInstance()->nextTick($processNextBatch);
207215
})
208-
->catch($reject)
209-
;
216+
->catch($reject);
210217
};
211218

212219
EventLoop::getInstance()->nextTick($processNextBatch);
213220
});
214221
}
222+
223+
/**
224+
* Wrap a task to ensure proper concurrency control.
225+
*
226+
* This method ensures all tasks use the await pattern for proper fiber-based concurrency:
227+
* - All callables are wrapped to ensure their results are awaited
228+
* - Promise instances are wrapped with await
229+
* - Other types are wrapped in a callable
230+
*
231+
* @param mixed $task The task to wrap
232+
* @return callable A callable that properly defers execution
233+
*/
234+
private function wrapTaskForConcurrency(mixed $task): callable
235+
{
236+
if (is_callable($task)) {
237+
return function () use ($task) {
238+
$result = $task();
239+
if ($result instanceof PromiseInterface) {
240+
return $this->awaitHandler->await($result);
241+
}
242+
return $result;
243+
};
244+
}
245+
246+
if ($task instanceof PromiseInterface) {
247+
return function () use ($task) {
248+
return $this->awaitHandler->await($task);
249+
};
250+
}
251+
252+
return function () use ($task) {
253+
return $task;
254+
};
255+
}
215256
}

0 commit comments

Comments
 (0)