Skip to content

perf: Use channels for DNS verification#11622

Open
hmacr wants to merge 4 commits into1.9.xfrom
ser-1162
Open

perf: Use channels for DNS verification#11622
hmacr wants to merge 4 commits into1.9.xfrom
ser-1162

Conversation

@hmacr
Copy link
Copy Markdown
Contributor

@hmacr hmacr commented Mar 24, 2026

Towards SER-1162

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

The DNS validator replaces its coroutine coordination from Swoole\Coroutine\WaitGroup plus a shared $failedValidator to a Swoole\Coroutine\Channel-based result flow. Each spawned coroutine creates its own BaseDNS validator, computes valid, and pushes ['valid' => $valid, 'validator' => $validator] to the channel. The main coroutine reads exactly count($this->dnsServers) results, and on the first result with valid === false copies the failing validator's count, value, reason, and records into the current object and returns false; if none fail it returns true.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing WaitGroup-based synchronization with Channel-based result passing for DNS verification.
Description check ✅ Passed The description references SER-1162, which is the issue this PR addresses, and is related to the changeset's DNS verification improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ser-1162

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR refactors DNS::isValid() to use a Swoole Channel instead of a WaitGroup for coordinating concurrent DNS validation across multiple servers, enabling early-exit as soon as the first server reports a failure.

Key changes and concerns:

  • Deadlock risk (critical): The try...finally block that guarded $wg->done() in the original implementation has been removed. If BaseDNS::isValid() throws any exception (network timeout, socket error, etc.), the goroutine exits without pushing to the channel, causing $channel->pop() in the main loop to block indefinitely.
  • Early return with in-flight goroutines: Returning false on the first failed result leaves remaining goroutines running in the background. This is safe because the channel is buffered to $serverCount capacity (goroutines can always push without blocking), but it is a subtle behavioral change from the original WaitGroup approach where all goroutines were fully awaited before returning.
  • The core logic—requiring all DNS servers to agree the record is valid—is preserved correctly.

Confidence Score: 2/5

  • Not safe to merge as-is; missing exception handling in goroutines can cause a deadlock under real-world DNS failure conditions.
  • The removal of the try/finally block is a regression: any uncaught exception inside a goroutine (e.g., a DNS timeout or network error thrown by the underlying client) will prevent a push to the channel, leaving the main coroutine blocked on pop() forever. This is a credible scenario in production DNS validation flows.
  • src/Appwrite/Network/Validator/DNS.php — the goroutine closure lacks exception handling.

Important Files Changed

Filename Overview
src/Appwrite/Network/Validator/DNS.php Replaces WaitGroup with a buffered Channel for concurrent DNS validation; introduces a deadlock risk by removing the try/finally exception guard that ensured all goroutines always pushed a result.

Reviews (1): Last reviewed commit: "perf: Use channels for DNS verification" | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/Appwrite/Network/Validator/DNS.php (4)

1-5: Missing strict type declaration.

Per coding guidelines, PHP files should use strict type declarations where applicable. Consider adding it after the opening tag.

Suggested fix
 <?php
+
+declare(strict_types=1);
 
 namespace Appwrite\Network\Validator;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Network/Validator/DNS.php` around lines 1 - 5, Add a strict
types declaration to the top of the PHP file: insert declare(strict_types=1);
immediately after the opening <?php tag in
src/Appwrite/Network/Validator/DNS.php so the file follows project coding
guidelines; no other changes to the namespace Appwrite\Network\Validator or use
statements (e.g., Channel) are required.

47-47: Consider adding a timeout to pop() to prevent indefinite blocking.

If a DNS query hangs, $channel->pop() will block indefinitely. Swoole channels support a timeout parameter: $channel->pop(timeout_in_seconds). Adding a reasonable timeout would improve resilience.

Suggested fix
-            $result = $channel->pop();
+            $result = $channel->pop(30); // 30 second timeout
+            if ($result === false) {
+                // Channel closed or timeout - treat as failure
+                $this->reason = 'DNS validation timed out';
+                return false;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Network/Validator/DNS.php` at line 47, The call to
$channel->pop() in DNS.php can block indefinitely if a DNS query hangs; update
the code where $channel->pop() is used (the DNS resolver logic) to pass a
reasonable timeout (e.g., $channel->pop($timeoutInSeconds)) and handle the
timeout case by checking for a false/empty result and returning/logging an error
or retrying accordingly so the resolver doesn't block forever.

35-44: Edge case: empty $dnsServers array passes validation without checking.

If $dnsServers is empty, $serverCount is 0, no coroutines spawn, and the method returns true without performing any DNS validation. While the caller (per context snippet 1) defaults to 8.8.8.8, the validator itself doesn't enforce this invariant.

Consider falling back to the parent class validation when no custom DNS servers are configured.

Suggested fix
     public function isValid(mixed $value): bool
     {
         $serverCount = \count($this->dnsServers);
+        
+        if ($serverCount === 0) {
+            return parent::isValid($value);
+        }
+        
         $channel = new Channel($serverCount);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Network/Validator/DNS.php` around lines 35 - 44, The DNS
validator currently counts $this->dnsServers and spawns coroutines with Channel
but if $this->dnsServers is empty it sets $serverCount=0, spawns nothing and
effectively returns success; fix by detecting an empty $this->dnsServers before
creating the Channel and, in that case, delegate to the parent validation (call
parent::isValid($value)) so the original/default DNS behavior is preserved; keep
the existing concurrent path (Channel, BaseDNS, isValid, validator) unchanged
for non-empty $this->dnsServers.

46-56: Early return leaves coroutines running unnecessarily.

When validation fails and return false is executed on line 54, remaining coroutines continue running their DNS queries. While the buffered channel prevents blocking (coroutines can push without waiting), this wastes resources by completing queries whose results are discarded.

Consider draining the channel before returning to ensure clean resource handling:

Suggested fix
         for ($i = 0; $i < $serverCount; $i++) {
             $result = $channel->pop();
             if (!$result['valid']) {
                 $failed = $result['validator'];
                 $this->count = $failed->count;
                 $this->value = $failed->value;
                 $this->reason = $failed->reason;
                 $this->records = $failed->records;
+                // Drain remaining results to allow proper cleanup
+                for ($j = $i + 1; $j < $serverCount; $j++) {
+                    $channel->pop();
+                }
                 return false;
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Network/Validator/DNS.php` around lines 46 - 56, The early
return inside the for loop after detecting a failed result leaves the other
coroutines running and their results buffered; modify the code around the loop
that iterates using $serverCount and $channel->pop() so that when a failure is
detected (the branch that sets $failed,
$this->count/$this->value/$this->reason/$this->records and is about to return
false) you first drain the channel of the remaining expected responses (e.g.,
loop to pop the remaining ($serverCount - $i - 1) items or while the channel has
items) to consume and discard pending results, then return false; ensure you
reference the existing variables $serverCount, $channel, $result and $i when
implementing the drain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Appwrite/Network/Validator/DNS.php`:
- Around line 1-5: Add a strict types declaration to the top of the PHP file:
insert declare(strict_types=1); immediately after the opening <?php tag in
src/Appwrite/Network/Validator/DNS.php so the file follows project coding
guidelines; no other changes to the namespace Appwrite\Network\Validator or use
statements (e.g., Channel) are required.
- Line 47: The call to $channel->pop() in DNS.php can block indefinitely if a
DNS query hangs; update the code where $channel->pop() is used (the DNS resolver
logic) to pass a reasonable timeout (e.g., $channel->pop($timeoutInSeconds)) and
handle the timeout case by checking for a false/empty result and
returning/logging an error or retrying accordingly so the resolver doesn't block
forever.
- Around line 35-44: The DNS validator currently counts $this->dnsServers and
spawns coroutines with Channel but if $this->dnsServers is empty it sets
$serverCount=0, spawns nothing and effectively returns success; fix by detecting
an empty $this->dnsServers before creating the Channel and, in that case,
delegate to the parent validation (call parent::isValid($value)) so the
original/default DNS behavior is preserved; keep the existing concurrent path
(Channel, BaseDNS, isValid, validator) unchanged for non-empty
$this->dnsServers.
- Around line 46-56: The early return inside the for loop after detecting a
failed result leaves the other coroutines running and their results buffered;
modify the code around the loop that iterates using $serverCount and
$channel->pop() so that when a failure is detected (the branch that sets
$failed, $this->count/$this->value/$this->reason/$this->records and is about to
return false) you first drain the channel of the remaining expected responses
(e.g., loop to pop the remaining ($serverCount - $i - 1) items or while the
channel has items) to consume and discard pending results, then return false;
ensure you reference the existing variables $serverCount, $channel, $result and
$i when implementing the drain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c413ff57-53fe-4e3d-8246-79eb464dbefe

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5d7a5 and bd1f9bb.

📒 Files selected for processing (1)
  • src/Appwrite/Network/Validator/DNS.php

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit bd1f9bb - 3 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.48s Logs
LegacyConsoleClientTest::testListDocumentsWithCache 1 1.37s Logs
LegacyCustomClientTest::testUpdatedBefore 1 240.44s Logs
Commit fde321a - 4 flaky tests
Test Retries Total Time Details
AccountCustomClientTest::testAccountAbuseReset 1 6.50s Logs
UsageTest::testVectorsDBStats 1 10.10s Logs
RealtimeConsoleClientTest::testCreateDeployment 1 2.10s Logs
RealtimeCustomClientQueryTest::testDatabaseChannelWithQuery 1 30.21s Logs
Commit 82f734f - 4 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.04s Logs
DocumentsDBConsoleClientTest::testTimeout 1 120.05s Logs
DocumentsDBCustomClientTest::testTimeout 1 120.05s Logs
DocumentsDBCustomServerTest::testTimeout 1 120.04s Logs

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

✨ Benchmark results

  • Requests per second: 1,741
  • Requests with 200 status code: 313,448
  • P99 latency: 0.100014035

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,741 1,223
200 313,448 220,113
P99 0.100014035 0.181796097

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/unit/Network/Validators/DNSTest.php (2)

13-13: Consider extracting a coroutine test helper to remove duplication.

The repeated \Swoole\Coroutine\run(function () { ... }) wrapper appears in all four tests and can be centralized for readability.

🧩 Suggested DRY refactor
 class DNSTest extends TestCase
 {
+    private function runInCoroutine(callable $test): void
+    {
+        \Swoole\Coroutine\run($test);
+    }
+
     public function testSingleDNSServer(): void
     {
-        \Swoole\Coroutine\run(function () {
+        $this->runInCoroutine(function (): void {
             $validator = new DNS('appwrite.io', Record::TYPE_CNAME, ['8.8.8.8']);
             ...
         });
     }

Also applies to: 24-24, 35-35, 50-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/Network/Validators/DNSTest.php` at line 13, Extract the repeated
\Swoole\Coroutine\run(function () { ... }) wrapper into a single helper method
on the DNSTest class (e.g., private function runCoroutine(callable $fn) or
coroutineRun(callable $fn)) and replace each inline \Swoole\Coroutine\run(...)
occurrence in the DNSTest test methods with $this->runCoroutine(function() { ...
}); ensure the helper simply delegates to \Swoole\Coroutine\run($fn) so test
bodies remain unchanged and duplication is removed.

16-17: Prefer boolean-specific assertions over assertEquals(false, ...).

At Line 16, Line 17, Line 27, Line 28, Line 40, Line 55, and Line 58, assertFalse(...) is clearer and avoids loose comparisons.

♻️ Suggested assertion refinement
-            $this->assertEquals(false, $validator->isValid(''));
-            $this->assertEquals(false, $validator->isValid(null));
+            $this->assertFalse($validator->isValid(''));
+            $this->assertFalse($validator->isValid(null));
-            $this->assertEquals(false, $result);
+            $this->assertFalse($result);

Also applies to: 27-28, 40-40, 55-55, 58-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/Network/Validators/DNSTest.php` around lines 16 - 17, Replace
loose boolean equality assertions with boolean-specific PHPUnit assertions in
DNSTest.php: swap assertEquals(false, $validator->isValid(...)) calls to
assertFalse($validator->isValid(...)) (and any assertEquals(true, ...) to
assertTrue(...) if present) for the tests that call $validator->isValid('') and
$validator->isValid(null) (and the other occurrences mentioned). Update the
assertions that reference the validator->isValid(...) calls so they use
assertFalse/assertTrue to make the tests explicit and avoid loose comparisons.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/unit/Network/Validators/DNSTest.php`:
- Line 13: Extract the repeated \Swoole\Coroutine\run(function () { ... })
wrapper into a single helper method on the DNSTest class (e.g., private function
runCoroutine(callable $fn) or coroutineRun(callable $fn)) and replace each
inline \Swoole\Coroutine\run(...) occurrence in the DNSTest test methods with
$this->runCoroutine(function() { ... }); ensure the helper simply delegates to
\Swoole\Coroutine\run($fn) so test bodies remain unchanged and duplication is
removed.
- Around line 16-17: Replace loose boolean equality assertions with
boolean-specific PHPUnit assertions in DNSTest.php: swap assertEquals(false,
$validator->isValid(...)) calls to assertFalse($validator->isValid(...)) (and
any assertEquals(true, ...) to assertTrue(...) if present) for the tests that
call $validator->isValid('') and $validator->isValid(null) (and the other
occurrences mentioned). Update the assertions that reference the
validator->isValid(...) calls so they use assertFalse/assertTrue to make the
tests explicit and avoid loose comparisons.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd18e7e1-1ee2-4f28-9e51-309a22c52c07

📥 Commits

Reviewing files that changed from the base of the PR and between bd1f9bb and fde321a.

📒 Files selected for processing (1)
  • tests/unit/Network/Validators/DNSTest.php

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/unit/Network/Validators/DNSTest.php (2)

19-24: These “unit” tests are network-dependent and likely flaky.

Using public DNS servers and environment-specific resolver IPs makes results dependent on network and infra state. Consider isolating resolver behavior behind mocks/fakes for deterministic unit coverage, and keep live DNS checks in integration tests.

Also applies to: 30-35, 41-49, 57-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/Network/Validators/DNSTest.php` around lines 19 - 24, The tests in
DNSTest use real network resolvers (creating DNS('appwrite.io',
Record::TYPE_CNAME, ['8.8.8.8'])) which makes them flaky; change these unit
tests to use a mock/fake DNS resolver or inject a resolver dependency into the
DNS validator so you can stub responses deterministically rather than calling
real DNS. Update tests referencing DNS, Record::TYPE_CNAME, DNS::isValid and
DNS::getType to instantiate DNS with a test resolver or replace the resolver
calls with a fake that returns controlled answers; alternatively move the
existing live-resolution assertions into an integration test suite and keep unit
tests solely asserting behavior against the mocked resolver.

11-14: Add a skip guard for Swoole Coroutine availability to match the pattern used elsewhere in the test suite.

This test directly calls \Swoole\Coroutine\run() without checking if Coroutine is available. While ext-swoole is a required dependency, other test files (e.g., RealtimeCustomClientTest.php) already implement skip guards for coroutine usage. Use class_exists(\Swoole\Coroutine::class) to stay consistent with the existing pattern in the codebase:

Suggested patch
     private function runInCoroutine(callable $test): void
     {
+        if (!class_exists(\Swoole\Coroutine::class)) {
+            self::markTestSkipped('Swoole Coroutine is required for DNS tests.');
+        }
+
         \Swoole\Coroutine\run($test);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/Network/Validators/DNSTest.php` around lines 11 - 14, The private
helper runInCoroutine currently calls \Swoole\Coroutine::run() unguarded; update
runInCoroutine to first check class_exists(\Swoole\Coroutine::class) and call
$this->markTestSkipped('Swoole Coroutine not available') when false, otherwise
proceed to \Swoole\Coroutine::run($test); ensure you modify the runInCoroutine
method in the test class (the method named runInCoroutine) to implement this
skip-guard pattern used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/Network/Validators/DNSTest.php`:
- Around line 11-14: The new helper runInCoroutine lacks a PHPDoc; add a short
PHPDoc block above the runInCoroutine method describing its purpose (helper to
execute tests inside a Swoole coroutine) and the callable signature/type
(callable $test): void, noting it runs the provided callable via
\Swoole\Coroutine\run and that it does not return a value; place the PHPDoc
immediately above the private function runInCoroutine(callable $test): void so
tools and readers see the intent and expected parameter type.

---

Nitpick comments:
In `@tests/unit/Network/Validators/DNSTest.php`:
- Around line 19-24: The tests in DNSTest use real network resolvers (creating
DNS('appwrite.io', Record::TYPE_CNAME, ['8.8.8.8'])) which makes them flaky;
change these unit tests to use a mock/fake DNS resolver or inject a resolver
dependency into the DNS validator so you can stub responses deterministically
rather than calling real DNS. Update tests referencing DNS, Record::TYPE_CNAME,
DNS::isValid and DNS::getType to instantiate DNS with a test resolver or replace
the resolver calls with a fake that returns controlled answers; alternatively
move the existing live-resolution assertions into an integration test suite and
keep unit tests solely asserting behavior against the mocked resolver.
- Around line 11-14: The private helper runInCoroutine currently calls
\Swoole\Coroutine::run() unguarded; update runInCoroutine to first check
class_exists(\Swoole\Coroutine::class) and call $this->markTestSkipped('Swoole
Coroutine not available') when false, otherwise proceed to
\Swoole\Coroutine::run($test); ensure you modify the runInCoroutine method in
the test class (the method named runInCoroutine) to implement this skip-guard
pattern used elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18a3f08e-ef9a-46b7-956e-7988a0b89a02

📥 Commits

Reviewing files that changed from the base of the PR and between fde321a and 82f734f.

📒 Files selected for processing (1)
  • tests/unit/Network/Validators/DNSTest.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant