Skip to content

Commit afe00ee

Browse files
author
mingsing
committed
add healthcheck test
Signed-off-by: mingsing <[email protected]>
1 parent a91578d commit afe00ee

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed
File renamed without changes.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Copyright 2025 The Dapr Authors
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
"""
15+
import asyncio
16+
import time
17+
import unittest
18+
from unittest.mock import patch, MagicMock
19+
20+
from dapr.aio.clients.health import DaprHealth
21+
from dapr.conf import settings
22+
from dapr.version import __version__
23+
24+
25+
class DaprHealthCheckAsyncTests(unittest.IsolatedAsyncioTestCase):
26+
@patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'http://domain.com:3500')
27+
@patch('urllib.request.urlopen')
28+
async def test_wait_until_ready_success(self, mock_urlopen):
29+
mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200)
30+
31+
try:
32+
await DaprHealth.wait_until_ready()
33+
except Exception as e:
34+
self.fail(f'wait_until_ready() raised an exception unexpectedly: {e}')
35+
36+
mock_urlopen.assert_called_once()
37+
38+
called_url = mock_urlopen.call_args[0][0].full_url
39+
self.assertEqual(called_url, 'http://domain.com:3500/v1.0/healthz/outbound')
40+
41+
# Check headers are properly set
42+
headers = mock_urlopen.call_args[0][0].headers
43+
self.assertIn('User-agent', headers)
44+
self.assertEqual(headers['User-agent'], f'dapr-sdk-python/{__version__}')
45+
46+
@patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'http://domain.com:3500')
47+
@patch.object(settings, 'DAPR_API_TOKEN', 'mytoken')
48+
@patch('urllib.request.urlopen')
49+
async def test_wait_until_ready_success_with_api_token(self, mock_urlopen):
50+
mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200)
51+
52+
try:
53+
await DaprHealth.wait_until_ready()
54+
except Exception as e:
55+
self.fail(f'wait_until_ready() raised an exception unexpectedly: {e}')
56+
57+
mock_urlopen.assert_called_once()
58+
59+
# Check headers are properly set
60+
headers = mock_urlopen.call_args[0][0].headers
61+
self.assertIn('User-agent', headers)
62+
self.assertEqual(headers['User-agent'], f'dapr-sdk-python/{__version__}')
63+
self.assertIn('Dapr-api-token', headers)
64+
self.assertEqual(headers['Dapr-api-token'], 'mytoken')
65+
66+
@patch.object(settings, 'DAPR_HEALTH_TIMEOUT', '2.5')
67+
@patch('urllib.request.urlopen')
68+
async def test_wait_until_ready_timeout(self, mock_urlopen):
69+
mock_urlopen.return_value.__enter__.return_value = MagicMock(status=500)
70+
71+
start = time.time()
72+
73+
with self.assertRaises(TimeoutError):
74+
await DaprHealth.wait_until_ready()
75+
76+
self.assertGreaterEqual(time.time() - start, 2.5)
77+
self.assertGreater(mock_urlopen.call_count, 1)
78+
79+
@patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'http://domain.com:3500')
80+
@patch.object(settings, 'DAPR_HEALTH_TIMEOUT', '5.0')
81+
@patch('urllib.request.urlopen')
82+
async def test_health_check_does_not_block(self, mock_urlopen):
83+
"""Test that health check doesn't block other async tasks from running"""
84+
# Mock health check to retry several times before succeeding
85+
call_count = [0] # Use list to allow modification in nested function
86+
87+
class MockResponse:
88+
def __init__(self, status):
89+
self.status = status
90+
91+
def __enter__(self):
92+
return self
93+
94+
def __exit__(self, exc_type, exc_val, exc_tb):
95+
return None
96+
97+
def side_effect(*args, **kwargs):
98+
call_count[0] += 1
99+
# First 2 calls fail with URLError, then succeed
100+
# This will cause ~2 seconds of retries (1 second sleep after each failure)
101+
if call_count[0] <= 2:
102+
import urllib.error
103+
raise urllib.error.URLError('Connection refused')
104+
else:
105+
return MockResponse(status=200)
106+
107+
mock_urlopen.side_effect = side_effect
108+
109+
# Counter that will be incremented by background task
110+
counter = [0] # Use list to allow modification in nested function
111+
is_running = [True]
112+
113+
async def increment_counter():
114+
"""Background task that increments counter every 0.5 seconds"""
115+
while is_running[0]:
116+
await asyncio.sleep(0.5)
117+
counter[0] += 1
118+
119+
# Start the background task
120+
counter_task = asyncio.create_task(increment_counter())
121+
122+
try:
123+
# Run health check (will take ~2 seconds with retries)
124+
await DaprHealth.wait_until_ready()
125+
126+
# Stop the background task
127+
is_running[0] = False
128+
await asyncio.sleep(0.1) # Give it time to finish current iteration
129+
130+
# Verify the counter was incremented during health check
131+
# In 2 seconds with 0.5s intervals, we expect at least 3 increments
132+
self.assertGreaterEqual(
133+
counter[0],
134+
3,
135+
f'Expected counter to increment at least 3 times during health check, '
136+
f'but got {counter[0]}. This indicates health check may be blocking.',
137+
)
138+
139+
# Verify health check made multiple attempts
140+
self.assertGreaterEqual(call_count[0], 2)
141+
142+
finally:
143+
# Clean up
144+
is_running[0] = False
145+
counter_task.cancel()
146+
try:
147+
await counter_task
148+
except asyncio.CancelledError:
149+
pass
150+
151+
@patch.object(settings, 'DAPR_HTTP_ENDPOINT', 'http://domain.com:3500')
152+
@patch('urllib.request.urlopen')
153+
async def test_multiple_health_checks_concurrent(self, mock_urlopen):
154+
"""Test that multiple health check calls can run concurrently"""
155+
mock_urlopen.return_value.__enter__.return_value = MagicMock(status=200)
156+
157+
# Run multiple health checks concurrently
158+
start_time = time.time()
159+
results = await asyncio.gather(
160+
DaprHealth.wait_until_ready(),
161+
DaprHealth.wait_until_ready(),
162+
DaprHealth.wait_until_ready(),
163+
)
164+
elapsed = time.time() - start_time
165+
166+
# All should complete successfully
167+
self.assertEqual(len(results), 3)
168+
self.assertIsNone(results[0])
169+
self.assertIsNone(results[1])
170+
self.assertIsNone(results[2])
171+
172+
# Should complete quickly since they run concurrently
173+
self.assertLess(elapsed, 1.0)
174+
175+
# Verify multiple calls were made
176+
self.assertGreaterEqual(mock_urlopen.call_count, 3)
177+
178+
179+
if __name__ == '__main__':
180+
unittest.main()

0 commit comments

Comments
 (0)