Skip to content
This repository has been archived by the owner on Feb 29, 2024. It is now read-only.

Commit

Permalink
feat: Add active debuggee support. (#54)
Browse files Browse the repository at this point in the history
The Firebase client now adds two new fields to the Debuggee entry in
the Firebase RTDB, `registrationTimeMsec` and `lastUpdateTimeMsec`.
The first time the Debuggee entry is written to the RTDB, both fields
are set. Thereafter only the `lastUpdateTimeMsec` will be changed, which
is done periodically every hour when the agent is running continuously,
it is also updated when the agent first starts up.
This is in support of
GoogleCloudPlatform/snapshot-debugger#76.
  • Loading branch information
jasonborg authored Dec 5, 2022
1 parent d209fb8 commit db06239
Show file tree
Hide file tree
Showing 2 changed files with 462 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,25 @@
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

class FirebaseClient implements HubClient {
static final Duration MARK_DEBUGGEE_ACTIVE_PERIOD = Duration.ofHours(1);

// Note, the Debuggee class is to be serialized automatically by the Firebase library. To support
// this the following is purposely done here:
// 1. All data members are public.
Expand All @@ -66,6 +72,8 @@ static class Debuggee {
public Map<String, String> labels;
public String agentVersion;
public List<Map<String, Object>> sourceContexts;
public final Map<String, String> registrationTimeMsec = ServerValue.TIMESTAMP;
public final Map<String, String> lastUpdateTimeMsec = ServerValue.TIMESTAMP;

public void setDebuggeeId(String debuggeeId) {
this.id = debuggeeId;
Expand Down Expand Up @@ -114,23 +122,29 @@ static class Timeout {
this.units = units;
}

public String toString() {
return value + " " + units;
}

long value;
TimeUnit units;
}
;

/** The timeouts are configurable to allow the unit test to set lower timeouts */
/** The timeouts are configurable to allow the unit test to set lower values */
static class TimeoutConfig {
TimeoutConfig(long value, TimeUnit units) {
this.setDebuggee = new Timeout(value, units);
this.setBreakpoint = new Timeout(value, units);
this.getSchemaVersion = new Timeout(value, units);
this.dbConfiguredTest = new Timeout(value, units);
this.debuggeePresentTest = new Timeout(value, units);
this.listActiveBreakpoints = new Timeout(value, units);
}

Timeout setDebuggee;
Timeout setBreakpoint;
Timeout getSchemaVersion;
Timeout dbConfiguredTest;
Timeout debuggeePresentTest;
Timeout listActiveBreakpoints;
}
;
Expand Down Expand Up @@ -311,6 +325,9 @@ public FirebaseDatabase getDbInstance(FirebaseApp app) {
.serializeNulls()
.create();

private Timer markDebuggeeActiveTimer;
private final Duration markDebuggeeActivePeriod;

/** Default constructor using environment provided by {@link GcpEnvironment}. */
public FirebaseClient() {
this.metadata = GcpEnvironment.getMetadataQuery();
Expand All @@ -320,7 +337,13 @@ public FirebaseClient() {

// Initialize everything with the same timeout, then make specific overrides where desired.
this.timeouts = new TimeoutConfig(30, TimeUnit.SECONDS);
this.timeouts.getSchemaVersion.value = 10;

// We lower this timeout, as it's only used when probing a potential DB address to see if it
// exists and is configured for Snapshot Debugger use, so it's expected it may fail. It will
// also be retried if no configured DBs are found.
this.timeouts.dbConfiguredTest.value = 10;

this.markDebuggeeActivePeriod = MARK_DEBUGGEE_ACTIVE_PERIOD;
}

/**
Expand All @@ -330,19 +353,22 @@ public FirebaseClient() {
* @param classPathLookup read and explore application resources
* @param labels debuggee labels (such as version and module)
* @param firebaseStaticWrappers implementation of Firebase static api calls
* @param the timeout values to use
* @param timeouts the timeout values to use
* @param markDebuggeeActivePeriod the period to use for marking the debuggee active
*/
public FirebaseClient(
MetadataQuery metadata,
ClassPathLookup classPathLookup,
Map<String, String> labels,
FirebaseStaticWrappers firebaseStaticWrappers,
TimeoutConfig timeouts) {
TimeoutConfig timeouts,
Duration markDebuggeeActivePeriod) {
this.metadata = metadata;
this.classPathLookup = classPathLookup;
this.labels = labels;
this.firebaseStaticWrappers = firebaseStaticWrappers;
this.timeouts = timeouts;
this.markDebuggeeActivePeriod = markDebuggeeActivePeriod;
}

@Override
Expand All @@ -361,10 +387,21 @@ public boolean registerDebuggee(Map<String, String> extraDebuggeeLabels) throws

Debuggee debuggee = getDebuggeeInfo(extraDebuggeeLabels);
setDebuggeeId(debuggee.id);
String debuggeePath = "cdbg/debuggees/" + getDebuggeeId();
setDbValue(debuggeePath, debuggee, timeouts.setDebuggee);

if (isDebuggeePresentInDb(
this.firebaseApp, this.firebaseStaticWrappers, debuggee.id, timeouts.debuggeePresentTest)) {
infofmt("Debuggee %s is already present in the RTDB, marking it active", getDebuggeeId());
markDebuggeeActive();
} else {
// Note, no need to update the lastUpdateTimeMsec field of the Debuggee in the RTDB, below we
// start the markDebuggeeActiveTimer, it will schecule the first update right away with no
// delay.
infofmt("Debuggee %s is not yet present in the RTDB, sending it.", getDebuggeeId());
setDbValue(getDebuggeeDbPath(debuggee.id), debuggee, timeouts.setDebuggee);
}

registerActiveBreakpointListener();
startMarkDebuggeeActiveTimer();
isRegistered = true;

infofmt(
Expand Down Expand Up @@ -577,6 +614,7 @@ public boolean isEnabled() {
public void shutdown() {
infofmt("FirebaseClient::shutdown() begin");
isShutdown = true;
stopMarkDebuggeeActiveTimer();
metadata.shutdown();
unregisterActiveBreakpointListener();
deleteFirebaseApp();
Expand All @@ -593,6 +631,14 @@ private void setDebuggeeId(String id) {
debuggeeId = id;
}

public String getDebuggeeDbPath() {
return getDebuggeeDbPath(getDebuggeeId());
}

public static String getDebuggeeDbPath(String debuggeeId) {
return "cdbg/debuggees/" + debuggeeId;
}

/**
* Fills in the debuggee registration request message.
*
Expand Down Expand Up @@ -799,13 +845,7 @@ private FirebaseApp initializeFirebaseAppForUrl(String databaseUrl) {
"Attempting to verify if db %s exists and is configured for the Snapshot Debugger",
databaseUrl);

Object value =
getDbValue(
app, this.firebaseStaticWrappers, "cdbg/schema_version", timeouts.getSchemaVersion);

if (value != null) {
// For our purposes, we don't care what the data is, as long long as it's not null it
// indicates the DB exists and has been initialized for Snapshot Debugger use.
if (isDbConfigured(app, this.firebaseStaticWrappers, timeouts.dbConfiguredTest)) {
infofmt("Successfully initialized FirebaseApp with db '%s'", databaseUrl);
isAppGoodToUse = true;
}
Expand Down Expand Up @@ -912,11 +952,7 @@ public void onDataChange(DataSnapshot dataSnapshot) {
@Override
public void onCancelled(DatabaseError error) {
warnfmt("Database subscription error: %s", error.getMessage());

// This will force listActiveBreakpoints() to throw an exception and get the native
// agent code to begin calling registerDebuggee() again so the agent code can get
// back to a working registered state.
isRegistered = false;
forceReregistration();
}
});
}
Expand All @@ -928,6 +964,55 @@ synchronized void unregisterActiveBreakpointListener() {
}
}

private void markDebuggeeActive() throws Exception {
String lastUpdateTimeMsecPath = getDebuggeeDbPath() + "/lastUpdateTimeMsec";
setDbValue(lastUpdateTimeMsecPath, ServerValue.TIMESTAMP, timeouts.setDebuggee);
}

private void forceReregistration() {
// This will force listActiveBreakpoints() to throw an exception and get the native agent code
// to begin calling registerDebuggee() again so the agent code can get back to a working
// registered state.
isRegistered = false;
}

private void startMarkDebuggeeActiveTimer() {
if (markDebuggeeActiveTimer != null) {
return;
}

markDebuggeeActiveTimer = new Timer();

markDebuggeeActiveTimer.scheduleAtFixedRate(
new TimerTask() {
@Override
public void run() {
if (!isRegistered) {
return;
}

try {
markDebuggeeActive();
} catch (Exception e) {
warnfmt("An unexpected error occurred trying to mark the debuggee active: ", e);
forceReregistration();
}
}
},
// We wait for the first update as the registerDebuggee() handles marking the debuggee
// active
// initially.
/* delay= */ markDebuggeeActivePeriod.toMillis(),
/* period= */ markDebuggeeActivePeriod.toMillis());
}

private void stopMarkDebuggeeActiveTimer() {
if (markDebuggeeActiveTimer != null) {
markDebuggeeActiveTimer.cancel();
markDebuggeeActiveTimer = null;
}
}

/**
* Helper to set the data at a given path in the Firebase RTDB. Returns normally on success,
* throws an Exception on timeout or a write error.
Expand Down Expand Up @@ -957,7 +1042,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) {
// Returns null on timeout.
String error = result.poll(timeout.value, timeout.units);
if (error == null) {
error = "Timed out after " + timeout.value + " " + timeout.units.toString();
error = "Timed out after " + timeout;
}

if (!error.isEmpty()) {
Expand All @@ -970,6 +1055,29 @@ public void onComplete(DatabaseError error, DatabaseReference ref) {
infofmt("Firebase Database write operation to '%s' was successful", path);
}

private static boolean isDbConfigured(
FirebaseApp firebaseApp, FirebaseStaticWrappers firebaseStaticWrappers, Timeout timeout)
throws Exception {

// For our purposes, we don't care what the data is, as long long as it's not null it
// indicates the DB exists and has been initialized for Snapshot Debugger use.
return getDbValue(firebaseApp, firebaseStaticWrappers, "cdbg/schema_version", timeout) != null;
}

private static boolean isDebuggeePresentInDb(
FirebaseApp firebaseApp,
FirebaseStaticWrappers firebaseStaticWrappers,
String debuggeeId,
Timeout timeout)
throws Exception {

String registrationTimePath = getDebuggeeDbPath(debuggeeId) + "/registrationTimeMsec";

// For our purposes, we don't care what the data is, as long as it's not null it
// indicates the debuggee exists in the DB.
return getDbValue(firebaseApp, firebaseStaticWrappers, registrationTimePath, timeout) != null;
}

/**
* Helper to read the data at a given path in the Firebase RTDB. Returns the data on success,
* throws an Exception on timeout or a read error.
Expand All @@ -986,50 +1094,42 @@ private static Object getDbValue(
FirebaseStaticWrappers firebaseStaticWrappers,
final String path,
Timeout timeout)
throws Exception {
throws IOException, InterruptedException, TimeoutException {
DatabaseReference dbRef = firebaseStaticWrappers.getDbInstance(app).getReference(path);

infofmt("Beginning Firebase Database read operation at '%s'", path);

ValueEventListener listener = null;

final ArrayBlockingQueue<Boolean> resultObtained = new ArrayBlockingQueue<>(1);
final ArrayList<Object> obtainedValue = new ArrayList<>();

try {
dbRef.addValueEventListener(
new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Object value = dataSnapshot.getValue();
infofmt(
"Response obtained, data was %s found at %s", value == null ? "not" : "", path);

obtainedValue.add(value);
resultObtained.offer(Boolean.TRUE);
}
dbRef.addListenerForSingleValueEvent(
new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Object value = dataSnapshot.getValue();
infofmt("Response obtained, data was %s found at %s", value == null ? "not" : "", path);

@Override
public void onCancelled(DatabaseError error) {
warnfmt("Database subscription error: %s", error.getMessage());
resultObtained.offer(Boolean.FALSE);
}
});
obtainedValue.add(value);
resultObtained.offer(Boolean.TRUE);
}

// Returns null on timeout.
Boolean isSuccess = resultObtained.poll(timeout.value, timeout.units);
@Override
public void onCancelled(DatabaseError error) {
warnfmt("Database subscription error: %s", error.getMessage());
resultObtained.offer(Boolean.FALSE);
}
});

// null will be returned on read timeout.
if (isSuccess == null) {
infofmt("Read from %s timed out after %d %s", path, timeout.value, timeout.units);
throw new Exception("Read timed out");
} else if (!isSuccess) {
throw new Exception("Error occurred attempting to read from the DB");
}
} finally {
if (listener != null) {
dbRef.removeEventListener(listener);
}
// Returns null on timeout.
Boolean isSuccess = resultObtained.poll(timeout.value, timeout.units);

// null will be returned on read timeout.
if (isSuccess == null) {
infofmt("Read from %s timed out after %s", path, timeout);
throw new TimeoutException(
"Firebase Database read operation from '" + path + "' timed out after " + timeout);
} else if (!isSuccess) {
throw new IOException("Error occurred attempting to read from the DB");
}

return obtainedValue.get(0);
Expand Down
Loading

0 comments on commit db06239

Please sign in to comment.