Skip to content
This repository was archived by the owner on Jan 23, 2024. It is now read-only.

Commit d313854

Browse files
authored
feat: add active debuggee support (#64)
* add registration and update timestamps on register * add periodic marking of active debuggee fixes #63
1 parent 58e483d commit d313854

File tree

3 files changed

+137
-27
lines changed

3 files changed

+137
-27
lines changed

firebase-sample/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import googleclouddebugger
2-
googleclouddebugger.enable(use_firebase= True)
2+
3+
googleclouddebugger.enable(use_firebase=True)
34

45
from flask import Flask
56

67
app = Flask(__name__)
78

9+
810
@app.route("/")
911
def hello_world():
1012
return "<p>Hello World!</p>"
11-

src/googleclouddebugger/firebase_client.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ def __init__(self):
102102
self._transmission_thread = None
103103
self._transmission_thread_startup_lock = threading.Lock()
104104
self._transmission_queue = deque(maxlen=100)
105+
self._mark_active_timer = None
106+
self._mark_active_interval_sec = 60 * 60 # 1 hour in seconds
105107
self._new_updates = threading.Event()
106108
self._breakpoint_subscription = None
107109

@@ -206,7 +208,8 @@ def SetupAuth(self,
206208
try:
207209
r = requests.get(
208210
f'{_METADATA_SERVER_URL}/project/project-id',
209-
headers={'Metadata-Flavor': 'Google'})
211+
headers={'Metadata-Flavor': 'Google'},
212+
timeout=1)
210213
project_id = r.text
211214
except requests.exceptions.RequestException:
212215
native.LogInfo('Metadata server not available')
@@ -246,6 +249,10 @@ def Stop(self):
246249
self._transmission_thread.join()
247250
self._transmission_thread = None
248251

252+
if self._mark_active_timer is not None:
253+
self._mark_active_timer.cancel()
254+
self._mark_active_timer = None
255+
249256
if self._breakpoint_subscription is not None:
250257
self._breakpoint_subscription.close()
251258
self._breakpoint_subscription = None
@@ -302,6 +309,8 @@ def _MainThreadProc(self):
302309
subscription_required, delay = self._SubscribeToBreakpoints()
303310
self.subscription_complete.set()
304311

312+
self._StartMarkActiveTimer()
313+
305314
def _TransmissionThreadProc(self):
306315
"""Entry point for the transmission worker thread."""
307316

@@ -312,6 +321,22 @@ def _TransmissionThreadProc(self):
312321

313322
self._new_updates.wait(delay)
314323

324+
def _MarkActiveTimerFunc(self):
325+
"""Entry point for the mark active timer."""
326+
327+
try:
328+
self._MarkDebuggeeActive()
329+
except:
330+
native.LogInfo(
331+
f'Failed to mark debuggee as active: {traceback.format_exc()}')
332+
finally:
333+
self._StartMarkActiveTimer()
334+
335+
def _StartMarkActiveTimer(self):
336+
self._mark_active_timer = threading.Timer(self._mark_active_interval_sec,
337+
self._MarkActiveTimerFunc)
338+
self._mark_active_timer.start()
339+
315340
def _RegisterDebuggee(self):
316341
"""Single attempt to register the debuggee.
317342
@@ -334,12 +359,21 @@ def _RegisterDebuggee(self):
334359
return (True, self.register_backoff.Failed())
335360

336361
try:
337-
debuggee_path = f'cdbg/debuggees/{self._debuggee_id}'
338-
native.LogInfo(
339-
f'registering at {self._database_url}, path: {debuggee_path}')
340-
firebase_admin.db.reference(debuggee_path).set(debuggee)
362+
present = self._CheckDebuggeePresence()
363+
if present:
364+
self._MarkDebuggeeActive()
365+
else:
366+
debuggee_path = f'cdbg/debuggees/{self._debuggee_id}'
367+
native.LogInfo(
368+
f'registering at {self._database_url}, path: {debuggee_path}')
369+
debuggee_data = copy.deepcopy(debuggee)
370+
debuggee_data['registrationTimeUnixMsec'] = {'.sv': 'timestamp'}
371+
debuggee_data['lastUpdateTimeUnixMsec'] = {'.sv': 'timestamp'}
372+
firebase_admin.db.reference(debuggee_path).set(debuggee_data)
373+
341374
native.LogInfo(
342375
f'Debuggee registered successfully, ID: {self._debuggee_id}')
376+
343377
self.register_backoff.Succeeded()
344378
return (False, 0) # Proceed immediately to subscribing to breakpoints.
345379
except BaseException:
@@ -348,6 +382,26 @@ def _RegisterDebuggee(self):
348382
native.LogInfo(f'Failed to register debuggee: {traceback.format_exc()}')
349383
return (True, self.register_backoff.Failed())
350384

385+
def _CheckDebuggeePresence(self):
386+
path = f'cdbg/debuggees/{self._debuggee_id}/registrationTimeUnixMsec'
387+
try:
388+
snapshot = firebase_admin.db.reference(path).get()
389+
# The value doesn't matter; just return true if there's any value.
390+
return snapshot is not None
391+
except BaseException:
392+
native.LogInfo(
393+
f'Failed to check debuggee presence: {traceback.format_exc()}')
394+
return False
395+
396+
def _MarkDebuggeeActive(self):
397+
active_path = f'cdbg/debuggees/{self._debuggee_id}/lastUpdateTimeUnixMsec'
398+
try:
399+
server_time = {'.sv': 'timestamp'}
400+
firebase_admin.db.reference(active_path).set(server_time)
401+
except BaseException:
402+
native.LogInfo(
403+
f'Failed to mark debuggee active: {traceback.format_exc()}')
404+
351405
def _SubscribeToBreakpoints(self):
352406
# Kill any previous subscriptions first.
353407
if self._breakpoint_subscription is not None:
@@ -374,7 +428,7 @@ def _ActiveBreakpointCallback(self, event):
374428
if event.path != '/':
375429
breakpoint_id = event.path[1:]
376430
# Breakpoint may have already been deleted, so pop for possible no-op.
377-
self._breakpoints.pop(breakpoint_id, None)
431+
self._breakpoints.pop(breakpoint_id, None)
378432
else:
379433
if event.path == '/':
380434
# New set of breakpoints.

tests/firebase_client_test.py

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Unit tests for firebase_client module."""
22

3+
import copy
34
import os
45
import sys
56
import tempfile
@@ -77,10 +78,14 @@ def setUp(self):
7778
self.addCleanup(patcher.stop)
7879

7980
# Set up the mocks for the database refs.
81+
self._mock_presence_ref = MagicMock()
82+
self._mock_presence_ref.get.return_value = None
83+
self._mock_active_ref = MagicMock()
8084
self._mock_register_ref = MagicMock()
8185
self._fake_subscribe_ref = FakeReference()
8286
self._mock_db_ref.side_effect = [
83-
self._mock_register_ref, self._fake_subscribe_ref
87+
self._mock_presence_ref, self._mock_register_ref,
88+
self._fake_subscribe_ref
8489
]
8590

8691
def tearDown(self):
@@ -139,18 +144,46 @@ def testStart(self):
139144
self._mock_initialize_app.assert_called_with(
140145
None, {'databaseURL': f'https://{TEST_PROJECT_ID}-cdbg.firebaseio.com'})
141146
self.assertEqual([
147+
call(f'cdbg/debuggees/{debuggee_id}/registrationTimeUnixMsec'),
142148
call(f'cdbg/debuggees/{debuggee_id}'),
143149
call(f'cdbg/breakpoints/{debuggee_id}/active')
144150
], self._mock_db_ref.call_args_list)
145151

146152
# Verify that the register call has been made.
147-
self._mock_register_ref.set.assert_called_once_with(
148-
self._client._GetDebuggee())
153+
expected_data = copy.deepcopy(self._client._GetDebuggee())
154+
expected_data['registrationTimeUnixMsec'] = {'.sv': 'timestamp'}
155+
expected_data['lastUpdateTimeUnixMsec'] = {'.sv': 'timestamp'}
156+
self._mock_register_ref.set.assert_called_once_with(expected_data)
157+
158+
def testStartAlreadyPresent(self):
159+
# Create a mock for just this test that claims the debuggee is registered.
160+
mock_presence_ref = MagicMock()
161+
mock_presence_ref.get.return_value = 'present!'
162+
163+
self._mock_db_ref.side_effect = [
164+
mock_presence_ref, self._mock_active_ref, self._fake_subscribe_ref
165+
]
166+
167+
self._client.SetupAuth(project_id=TEST_PROJECT_ID)
168+
self._client.Start()
169+
self._client.subscription_complete.wait()
170+
171+
debuggee_id = self._client._debuggee_id
172+
173+
self.assertEqual([
174+
call(f'cdbg/debuggees/{debuggee_id}/registrationTimeUnixMsec'),
175+
call(f'cdbg/debuggees/{debuggee_id}/lastUpdateTimeUnixMsec'),
176+
call(f'cdbg/breakpoints/{debuggee_id}/active')
177+
], self._mock_db_ref.call_args_list)
178+
179+
# Verify that the register call has been made.
180+
self._mock_active_ref.set.assert_called_once_with({'.sv': 'timestamp'})
149181

150182
def testStartRegisterRetry(self):
151-
# A new db ref is fetched on each retry.
183+
# A new set of db refs are fetched on each retry.
152184
self._mock_db_ref.side_effect = [
153-
self._mock_register_ref, self._mock_register_ref,
185+
self._mock_presence_ref, self._mock_register_ref,
186+
self._mock_presence_ref, self._mock_register_ref,
154187
self._fake_subscribe_ref
155188
]
156189

@@ -169,6 +202,7 @@ def testStartSubscribeRetry(self):
169202

170203
# A new db ref is fetched on each retry.
171204
self._mock_db_ref.side_effect = [
205+
self._mock_presence_ref,
172206
self._mock_register_ref,
173207
mock_subscribe_ref, # Fail the first time
174208
self._fake_subscribe_ref # Succeed the second time
@@ -178,7 +212,28 @@ def testStartSubscribeRetry(self):
178212
self._client.Start()
179213
self._client.subscription_complete.wait()
180214

181-
self.assertEqual(3, self._mock_db_ref.call_count)
215+
self.assertEqual(4, self._mock_db_ref.call_count)
216+
217+
def testMarkActiveTimer(self):
218+
# Make sure that there are enough refs queued up.
219+
refs = list(self._mock_db_ref.side_effect)
220+
refs.extend([self._mock_active_ref] * 10)
221+
self._mock_db_ref.side_effect = refs
222+
223+
# Speed things WAY up rather than waiting for hours.
224+
self._client._mark_active_interval_sec = 0.1
225+
226+
self._client.SetupAuth(project_id=TEST_PROJECT_ID)
227+
self._client.Start()
228+
self._client.subscription_complete.wait()
229+
230+
# wait long enough for the timer to trigger a few times.
231+
time.sleep(0.5)
232+
233+
print(f'Timer triggered {self._mock_active_ref.set.call_count} times')
234+
self.assertTrue(self._mock_active_ref.set.call_count > 3)
235+
self._mock_active_ref.set.assert_called_with({'.sv': 'timestamp'})
236+
182237

183238
def testBreakpointSubscription(self):
184239
# This class will keep track of the breakpoint updates and will check
@@ -219,12 +274,10 @@ def callback(self, new_breakpoints):
219274
},
220275
]
221276

222-
expected_results = [[breakpoints[0]],
223-
[breakpoints[0], breakpoints[1]],
277+
expected_results = [[breakpoints[0]], [breakpoints[0], breakpoints[1]],
224278
[breakpoints[0], breakpoints[1], breakpoints[2]],
225279
[breakpoints[1], breakpoints[2]],
226-
[breakpoints[1], breakpoints[2]]
227-
]
280+
[breakpoints[1], breakpoints[2]]]
228281
result_checker = ResultChecker(expected_results, self)
229282

230283
self._client.on_active_breakpoints_changed = result_checker.callback
@@ -257,8 +310,9 @@ def testEnqueueBreakpointUpdate(self):
257310
final_ref_mock = MagicMock()
258311

259312
self._mock_db_ref.side_effect = [
260-
self._mock_register_ref, self._fake_subscribe_ref, active_ref_mock,
261-
snapshot_ref_mock, final_ref_mock
313+
self._mock_presence_ref, self._mock_register_ref,
314+
self._fake_subscribe_ref, active_ref_mock, snapshot_ref_mock,
315+
final_ref_mock
262316
]
263317

264318
self._client.SetupAuth(project_id=TEST_PROJECT_ID)
@@ -316,13 +370,13 @@ def testEnqueueBreakpointUpdate(self):
316370
db_ref_calls = self._mock_db_ref.call_args_list
317371
self.assertEqual(
318372
call(f'cdbg/breakpoints/{debuggee_id}/active/{breakpoint_id}'),
319-
db_ref_calls[2])
373+
db_ref_calls[3])
320374
self.assertEqual(
321375
call(f'cdbg/breakpoints/{debuggee_id}/snapshot/{breakpoint_id}'),
322-
db_ref_calls[3])
376+
db_ref_calls[4])
323377
self.assertEqual(
324378
call(f'cdbg/breakpoints/{debuggee_id}/final/{breakpoint_id}'),
325-
db_ref_calls[4])
379+
db_ref_calls[5])
326380

327381
active_ref_mock.delete.assert_called_once()
328382
snapshot_ref_mock.set.assert_called_once_with(full_breakpoint)
@@ -333,8 +387,8 @@ def testEnqueueBreakpointUpdateWithLogpoint(self):
333387
final_ref_mock = MagicMock()
334388

335389
self._mock_db_ref.side_effect = [
336-
self._mock_register_ref, self._fake_subscribe_ref, active_ref_mock,
337-
final_ref_mock
390+
self._mock_presence_ref, self._mock_register_ref,
391+
self._fake_subscribe_ref, active_ref_mock, final_ref_mock
338392
]
339393

340394
self._client.SetupAuth(project_id=TEST_PROJECT_ID)
@@ -383,10 +437,10 @@ def testEnqueueBreakpointUpdateWithLogpoint(self):
383437
db_ref_calls = self._mock_db_ref.call_args_list
384438
self.assertEqual(
385439
call(f'cdbg/breakpoints/{debuggee_id}/active/{breakpoint_id}'),
386-
db_ref_calls[2])
440+
db_ref_calls[3])
387441
self.assertEqual(
388442
call(f'cdbg/breakpoints/{debuggee_id}/final/{breakpoint_id}'),
389-
db_ref_calls[3])
443+
db_ref_calls[4])
390444

391445
active_ref_mock.delete.assert_called_once()
392446
final_ref_mock.set.assert_called_once_with(output_breakpoint)
@@ -414,6 +468,7 @@ def testEnqueueBreakpointUpdateRetry(self):
414468
]
415469

416470
self._mock_db_ref.side_effect = [
471+
self._mock_presence_ref,
417472
self._mock_register_ref,
418473
self._fake_subscribe_ref, # setup
419474
active_ref_mock, # attempt 1

0 commit comments

Comments
 (0)