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.
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 committed Dec 2, 2022
1 parent d209fb8 commit fef49f9
Show file tree
Hide file tree
Showing 2 changed files with 439 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,24 @@
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;

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 +71,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 @@ -119,18 +126,20 @@ static class Timeout {
}
;

/** 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.getDebuggee = new Timeout(value, units);
this.listActiveBreakpoints = new Timeout(value, units);
}

Timeout setDebuggee;
Timeout setBreakpoint;
Timeout getSchemaVersion;
Timeout getDebuggee;
Timeout listActiveBreakpoints;
}
;
Expand Down Expand Up @@ -311,6 +320,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 +332,12 @@ public FirebaseClient() {

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

// 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.
this.timeouts.getSchemaVersion.value = 10;

this.markDebuggeeActivePeriod = MARK_DEBUGGEE_ACTIVE_PERIOD;
}

/**
Expand All @@ -330,19 +347,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,12 +381,34 @@ 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);
String debuggeePath = getDebuggeeDbPath();
String registrationTimePath = debuggeePath + "/registrationTimeMsec";

boolean isDebuggeeAlreadyRegistered =
getDbValue(
this.firebaseApp,
this.firebaseStaticWrappers,
registrationTimePath,
timeouts.getDebuggee)
!= null;

if (isDebuggeeAlreadyRegistered) {
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(debuggeePath, debuggee, timeouts.setDebuggee);
}

registerActiveBreakpointListener();
isRegistered = true;

// Start the timer after setting isRegistered to true.
startMarkDebuggeeActiveTimer();

infofmt(
"Debuggee %s, registered %s, agent version: %s",
getDebuggeeId(), GSON.toJsonTree(debuggee), GcpDebugletVersion.VERSION);
Expand Down Expand Up @@ -577,6 +619,7 @@ public boolean isEnabled() {
public void shutdown() {
infofmt("FirebaseClient::shutdown() begin");
isShutdown = true;
stopMarkDebuggeeActiveTimer();
metadata.shutdown();
unregisterActiveBreakpointListener();
deleteFirebaseApp();
Expand All @@ -593,6 +636,10 @@ private void setDebuggeeId(String id) {
debuggeeId = id;
}

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

/**
* Fills in the debuggee registration request message.
*
Expand Down Expand Up @@ -912,11 +959,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 +971,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 @@ -991,45 +1083,42 @@ private static Object getDbValue(

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 %d %s", path, timeout.value, timeout.units);
throw new Exception(
"Firebase Database read operation from '"
+ path
+ "' timed out after "
+ timeout.value
+ " "
+ timeout.units.toString());
} else if (!isSuccess) {
throw new Exception("Error occurred attempting to read from the DB");
}

return obtainedValue.get(0);
Expand Down
Loading

0 comments on commit fef49f9

Please sign in to comment.