Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
428eedb
Initial code migration:
Apr 6, 2017
cfbbd6a
Reformatting code according to check-style plugin conventions
hiranya911 Apr 6, 2017
59842b8
Code reformatting: Fixing javadoc indentation
hiranya911 Apr 6, 2017
fef4ecf
More reformatting: Added google check style config
hiranya911 Apr 6, 2017
a606833
Code formatting continued
hiranya911 Apr 6, 2017
1d2ed82
Code formatting continued
hiranya911 Apr 6, 2017
8972b1b
Reformatting
hiranya911 Apr 6, 2017
e5509c3
Fixed all checkstyle violations; Build configured to fail on lint errors
Apr 6, 2017
0587d52
Updated checkstyle config
Apr 6, 2017
7f2f470
Working on re-enabling the integration tests
Apr 7, 2017
8ee5cfc
Temporarily removing IT test cases
Apr 7, 2017
dbc6ee6
Extending checkstyle coverage to test sourcesw
Apr 7, 2017
c66f35b
Cleaned up verbose test output
Apr 7, 2017
f7cbdc2
Adding 2 integration tests to verify the maven integration test support
Apr 7, 2017
7715b90
Moving test only trampolines to the test/ subtree; Adding a removed m…
Apr 10, 2017
c3591ed
Fixing integration tests
hiranya911 Apr 11, 2017
bc54329
Added event test
hiranya911 Apr 11, 2017
75df20f
Added the real time test
hiranya911 Apr 11, 2017
471d0a9
Adding data test back
hiranya911 Apr 11, 2017
f692bb6
Fixed the object serialization test
hiranya911 Apr 11, 2017
c368248
Adding a factory method to MapBuilder
hiranya911 Apr 11, 2017
a30ae95
Added transaction integration test
hiranya911 Apr 11, 2017
7714024
Added transaction test
hiranya911 Apr 11, 2017
2da7955
Added rules integration test
hiranya911 Apr 11, 2017
094d756
Updated rules test
hiranya911 Apr 11, 2017
f446c79
Merge branch 'hkj-initial-code-migration' into hkj-db-integration-tests
hiranya911 Apr 11, 2017
952c6b0
Cleaning up the test code; Removed unnecessary utils and constants
hiranya911 Apr 12, 2017
594bfbb
Added more factory methods for MapBuilder; Removed unused legacy depe…
hiranya911 Apr 12, 2017
5dc413f
Dropped the jackson test dependency
hiranya911 Apr 12, 2017
738c75e
Adding release profile
Apr 12, 2017
5e81a8b
Ignoring some maven specific temp files
Apr 12, 2017
922054d
Updated pom to use release + nexus plugin combo
Apr 12, 2017
337299f
Loading the version numbner from a Maven-filtered resource
Apr 12, 2017
aa88398
Adding the initial version of contribution guide
Apr 12, 2017
a0ef544
Merge branch 'hkj-db-integration-tests' into hkj-release-process
Apr 12, 2017
8b2ff85
Inferring the DB URL from project ID
Apr 12, 2017
2da5b03
Fixed test race condition
Apr 13, 2017
a24340c
Merge branch 'hkj-db-integration-tests' into hkj-release-process
Apr 13, 2017
7d39734
Fixing some race conditions in ITs
Apr 13, 2017
5f8e82a
Merge branch 'hkj-db-integration-tests' into hkj-release-process
Apr 13, 2017
4b0361c
Cleaning up EventHelper instances after use
hiranya911 Apr 13, 2017
e5eca5c
Merge branch 'hkj-db-integration-tests' into hkj-release-process
Apr 13, 2017
3d3be22
Adding back some logging code; Dropping fblocal URLs
Apr 13, 2017
eefc4ce
Dropping all references to fblocal
Apr 13, 2017
d13b1ec
Merge branch 'hkj-db-integration-tests' into hkj-release-process
Apr 13, 2017
e911387
Removing port numbers from test URLs
Apr 14, 2017
3c7f98f
Separated conjunctive assertions into separate ones for better error …
hiranya911 Apr 14, 2017
7efbd53
Merge branch 'hkj-db-integration-tests' into hkj-release-process
hiranya911 Apr 14, 2017
59d1689
Merge branch 'hkj-release-process' of github.com:firebase/firebase-ad…
Apr 14, 2017
0dc979c
Fixing merge conflict
Apr 14, 2017
b47dbf6
Allowing snapshot versions in database
Apr 14, 2017
78fdef7
Updated contributing guide
Apr 14, 2017
f5883d7
Updated titles and links
Apr 14, 2017
21e14d6
Fixing SCM URL
Apr 14, 2017
4bb3923
Removed Base64 and Preconditions
Apr 14, 2017
36b334f
Removed Objects
Apr 14, 2017
ca5af15
Implemented a mechanism to capture async errors and fail during integ…
Apr 14, 2017
1885188
Implementing App delete functionality
hiranya911 Apr 15, 2017
e8a1d83
Fixing some time sensitive tests; Added test cases for app delete
hiranya911 Apr 15, 2017
83410c2
Implemented delete related test cases; Using a single master app inst…
hiranya911 Apr 17, 2017
dc9b4ef
Merged with latest master
Apr 17, 2017
9ec7008
Updated app test
Apr 17, 2017
89d501a
Improved/simplified synchronization in FirebaseApp; Added more commen…
hiranya911 Apr 18, 2017
1d5c157
Throwing exceptions after calling destroy() on FirebaseDatabase
Apr 18, 2017
c47db63
Throwing errors from DatabaseReference instances after app delete
Apr 18, 2017
2e93f54
Merge branch 'master' into hkj-delete-apps
Apr 18, 2017
bfb96cb
Added delete checks to Repo
Apr 18, 2017
b50754f
Removing checks from Query as they eventually call Repo; Added more c…
Apr 18, 2017
ed059c4
Synchronizing the FirebaseDatabase.checkNotDeleted() method; Easing t…
Apr 19, 2017
28e4123
Removing the nested synch block in FirebaseApp.delete()
Apr 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 78 additions & 66 deletions src/main/java/com/google/firebase/FirebaseApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.firebase.internal.AuthStateListener;
import com.google.firebase.internal.FirebaseAppStore;
import com.google.firebase.internal.FirebaseExecutors;
import com.google.firebase.internal.FirebaseService;
import com.google.firebase.internal.GetTokenResult;
import com.google.firebase.internal.GuardedBy;
import com.google.firebase.internal.Joiner;
import com.google.firebase.internal.NonNull;
import com.google.firebase.internal.Nullable;
Expand All @@ -29,46 +29,50 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FYI- There are stray asterisks at the end of line 40 and 43 for some reason. They may show up in generated reference docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

import java.util.concurrent.atomic.AtomicReference;

/**
* The entry point of Firebase SDKs. It holds common configuration and state for Firebase APIs. Most
* applications don't need to directly interact with FirebaseApp. *
* applications don't need to directly interact with FirebaseApp.
*
* <p>Firebase APIs use the default FirebaseApp by default, unless a different one is explicitly
* passed to the API via FirebaseFoo.getInstance(firebaseApp). *
* passed to the API via FirebaseFoo.getInstance(firebaseApp).
*
* <p>{@link FirebaseApp#initializeApp(FirebaseOptions)} initializes the default app instance. This
* method should be invoked at startup.
*/
public class FirebaseApp {

/** A map of (name, FirebaseApp) instances. */
@GuardedBy("sLock")
private static final Map<String, FirebaseApp> instances = new HashMap<>();

public static final String DEFAULT_APP_NAME = "[DEFAULT]";
private static final long TOKEN_REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(55);
private static final TokenRefresher.Factory DEFAULT_TOKEN_REFRESHER_FACTORY =
new TokenRefresher.Factory();
private static final Object sLock = new Object();

/**
* Global lock for synchronizing all SDK-wide application state changes. Specifically, any
* accesses to instances map should be protected by this lock.
*/
private static final Object appsLock = new Object();

private final String name;
private final FirebaseOptions options;
private final TokenRefresher tokenRefresher;

private final AtomicBoolean deleted = new AtomicBoolean();

private final List<FirebaseAppLifecycleListener> lifecycleListeners =
new CopyOnWriteArrayList<>();

private final List<AuthStateListener> authStateListeners = new ArrayList<>();

private final AtomicReference<GetTokenResult> currentToken = new AtomicReference<>();
private final Map<String, FirebaseService> services = new HashMap<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm having some trouble reasoning about the threading of this class. I think it might help if you:

  • Consider using sLock as the one and only lock. This may be overkill, but it's much safer / simpler to only have to worry about a single lock (so you don't have potential for deadlocks, using the wrong lock, etc.), and I don't think this code is going to be subject to contention or be perf-critical so an overly-coarse lock is fine.
  • Consider just wrapping every single method in the class a lock rather than picking / choosing where to lock. Again, this makes the threading much more clear and error-proof.
  • Correctly use @GuardedBy or remove it. Right now "instances" is marked as @GuardedBy("sLock"), but not all of the usages are actually in a synchronized block, which is not consistent with the @GuardedBy docs. And if we're using @GuardedBy we should use it on the other members as well.
  • Add an overview comment explaining the threading strategy so I know what locks are expected to be held when and I can reason about the correctness of the strategy and then verify the implementation follows that strategy.

@hiranya911 hiranya911 Apr 18, 2017

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I prefer the idea of having two locks here:

  • A global lock for synchronizing access to the instances map
  • A local per-application lock for synchronizing application-level state changes

I basically don't want action from app-A to block an action from App-B.

I've added some comments explaining the purpose of two locks. Removed GuardedBy annotation. Also we need to selectively lock some parts of the code. In particular, we don't want to hold a lock while executing an event handler (e.g. AuthStateListener)


/**
* Per application lock for synchronizing all internal FirebaseApp state changes.
*/
private final Object lock = new Object();

/** Default constructor. */
private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory factory) {
Expand All @@ -81,7 +85,9 @@ private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory
/** Returns a mutable list of all FirebaseApps. */
public static List<FirebaseApp> getApps() {
// TODO(arondeak): reenable persistence. See b/28158809.
return new ArrayList<>(instances.values());
synchronized (appsLock) {
return ImmutableList.copyOf(instances.values());
}
}

/**
Expand All @@ -103,7 +109,7 @@ public static FirebaseApp getInstance() {
* #initializeApp(FirebaseOptions, String)} or {@link #getApps()}.
*/
public static FirebaseApp getInstance(@NonNull String name) {
synchronized (sLock) {
synchronized (appsLock) {
FirebaseApp firebaseApp = instances.get(normalize(name));
if (firebaseApp != null) {
return firebaseApp;
Expand Down Expand Up @@ -154,8 +160,8 @@ static FirebaseApp initializeApp(
FirebaseAppStore appStore = FirebaseAppStore.initialize();
String normalizedName = normalize(name);
final FirebaseApp firebaseApp;
synchronized (sLock) {
Preconditions.checkState(
synchronized (appsLock) {
checkState(
!instances.containsKey(normalizedName),
"FirebaseApp name " + normalizedName + " already exists!");

Expand All @@ -170,8 +176,12 @@ static FirebaseApp initializeApp(

@VisibleForTesting
static void clearInstancesForTest() {
// TODO(arondeak): also delete, once functionality is implemented.
synchronized (sLock) {
synchronized (appsLock) {
// Copy the instances list before iterating, as delete() would attempt to remove from the
// original list.
for (FirebaseApp app : ImmutableList.copyOf(instances.values())) {
app.delete();
}
instances.clear();
}
}
Expand All @@ -191,7 +201,7 @@ String getPersistenceKey() {

private static List<String> getAllAppNames() {
Set<String> allAppNames = new HashSet<>();
synchronized (sLock) {
synchronized (appsLock) {
for (FirebaseApp app : instances.values()) {
allAppNames.add(app.getName());
}
Expand Down Expand Up @@ -250,27 +260,33 @@ public String toString() {
*
* <p>A no-op if delete was called before.
*/
void delete() {
boolean valueChanged = deleted.compareAndSet(false /* expected */, true);
if (!valueChanged) {
return;
public void delete() {
synchronized (lock) {
boolean valueChanged = deleted.compareAndSet(false /* expected */, true);
if (!valueChanged) {
return;
}

for (FirebaseService service : services.values()) {
service.destroy();
}
services.clear();
authStateListeners.clear();
tokenRefresher.cleanup();
}
tokenRefresher.cleanup();

synchronized (sLock) {
instances.remove(this.name);
synchronized (appsLock) {
instances.remove(name);
}

FirebaseAppStore appStore = FirebaseAppStore.getInstance();
if (appStore != null) {
appStore.removeApp(name);
}

notifyOnAppDeleted();
}

private void checkNotDeleted() {
Preconditions.checkState(!deleted.get(), "FirebaseApp was deleted");
checkState(!deleted.get(), "FirebaseApp was deleted %s", this);
}

/**
Expand All @@ -293,10 +309,14 @@ public GetTokenResult then(@NonNull Task<String> task) throws Exception {
GetTokenResult oldToken = currentToken.get();
List<AuthStateListener> listenersCopy = null;
if (!newToken.equals(oldToken)) {
synchronized (authStateListeners) {
synchronized (lock) {
if (deleted.get()) {
return newToken;
}

// Grab the lock before compareAndSet to avoid a potential race
// condition
// with addAuthStateListener
// condition with addAuthStateListener. The same lock also ensures serial
// access to the token refresher.
if (currentToken.compareAndSet(oldToken, newToken)) {
listenersCopy = ImmutableList.copyOf(authStateListeners);
tokenRefresher.scheduleRefresh(TOKEN_REFRESH_INTERVAL_MILLIS);
Expand All @@ -318,55 +338,48 @@ boolean isDefaultApp() {
return DEFAULT_APP_NAME.equals(getName());
}

/**
* If an API has locally stored data it must register lifecycle listeners at initialization time.
*/
// TODO(arondeak): make sure that all APIs that are interested in these events are
// initialized using reflection when an app is deleted (for v5).
void addLifecycleEventListener(@NonNull FirebaseAppLifecycleListener listener) {
checkNotDeleted();
lifecycleListeners.add(checkNotNull(listener));
}

void removeLifecycleEventListener(@NonNull FirebaseAppLifecycleListener listener) {
checkNotDeleted();
lifecycleListeners.remove(checkNotNull(listener));
}

void addAuthStateListener(@NonNull final AuthStateListener listener) {
checkNotDeleted();
checkNotNull(listener);

GetTokenResult currentToken;
synchronized (authStateListeners) {
authStateListeners.add(listener);
synchronized (lock) {
checkNotDeleted();
authStateListeners.add(checkNotNull(listener));
currentToken = this.currentToken.get();
}

if (currentToken != null) {
// Task has copied the mAuthStateListeners before the listener was added.
// Task has copied the authStateListeners before the listener was added.
// Notify this listener explicitly.
listener.onAuthStateChanged(currentToken);
}
}

void removeAuthStateListener(@NonNull AuthStateListener listener) {
checkNotDeleted();
checkNotNull(listener);
synchronized (authStateListeners) {
authStateListeners.remove(listener);
synchronized (lock) {
checkNotDeleted();
authStateListeners.remove(checkNotNull(listener));
}
}

/**
* Notifies all listeners with the name and options of the deleted {@link FirebaseApp} instance.
*/
private void notifyOnAppDeleted() {
for (FirebaseAppLifecycleListener listener : lifecycleListeners) {
listener.onDeleted(name, options);
void addService(FirebaseService service) {
synchronized (lock) {
checkNotDeleted();
checkArgument(!services.containsKey(checkNotNull(service).getId()));
services.put(service.getId(), service);
}
}

FirebaseService getService(String id) {
synchronized (lock) {
checkArgument(!Strings.isNullOrEmpty(id));
return services.get(id);
}
}

/**
* Utility class for scheduling proactive token refresh events. Each FirebaseApp should have
* its own instance of this class. This class is not thread safe. The caller (FirebaseApp) must
* ensure that methods are called serially.
*/
static class TokenRefresher {

private final FirebaseApp firebaseApp;
Expand All @@ -382,7 +395,7 @@ static class TokenRefresher {
* @param delayMillis Duration in milliseconds, after which the token should be forcibly
* refreshed.
*/
final synchronized void scheduleRefresh(long delayMillis) {
final void scheduleRefresh(long delayMillis) {
cancelPrevious();
scheduleNext(
new Callable<Task<GetTokenResult>>() {
Expand Down Expand Up @@ -410,14 +423,13 @@ protected void scheduleNext(Callable<Task<GetTokenResult>> task, long delayMilli
}
}

protected synchronized void cleanup() {
protected void cleanup() {
if (future != null) {
future.cancel(true);
}
}

static class Factory {

TokenRefresher create(FirebaseApp app) {
return new TokenRefresher(app);
}
Expand Down
22 changes: 12 additions & 10 deletions src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.internal.AuthStateListener;
import com.google.firebase.internal.FirebaseService;
import com.google.firebase.internal.GetTokenResult;
import com.google.firebase.internal.NonNull;
import com.google.firebase.tasks.Task;
Expand Down Expand Up @@ -31,16 +32,6 @@ public static String getPersistenceKey(String name, FirebaseOptions options) {
return FirebaseApp.getPersistenceKey(name, options);
}

public static void addLifecycleEventListener(
@NonNull FirebaseApp app, @NonNull FirebaseAppLifecycleListener listener) {
app.addLifecycleEventListener(listener);
}

public static void removeLifecycleEventListener(
@NonNull FirebaseApp app, @NonNull FirebaseAppLifecycleListener listener) {
app.removeLifecycleEventListener(listener);
}

public static void addAuthStateChangeListener(
@NonNull FirebaseApp app, @NonNull AuthStateListener listener) {
app.addAuthStateListener(listener);
Expand All @@ -54,4 +45,15 @@ public static void removeAuthStateChangeListener(
public static Task<GetTokenResult> getToken(@NonNull FirebaseApp app, boolean forceRefresh) {
return app.getToken(forceRefresh);
}

public static <T extends FirebaseService> T getService(
@NonNull FirebaseApp app, @NonNull String id, @NonNull Class<T> type) {
return type.cast(app.getService(id));
}

public static <T extends FirebaseService> T addService(
@NonNull FirebaseApp app, @NonNull T service) {
app.addService(service);
return service;
}
}
Loading