Permalink
Browse files

Introducing `FallbackJSBundleLoader`

Reviewed By: michalgr

Differential Revision: D4386951

fbshipit-source-id: b1375deee9b3268d414e1b03fa79df50ac4d36cb
  • Loading branch information...
amnn authored and facebook-github-bot committed Jan 13, 2017
1 parent 89d72c9 commit c3892fa87141745eed3c07bd02138576826053f1
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.cxxbridge;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Stack;
+
+/**
+ * FallbackJSBundleLoader
+ *
+ * An implementation of {@link JSBundleLoader} that will try to load from
+ * multiple sources, falling back from one source to the next at load time
+ * when an exception is thrown for a recoverable error.
+ */
+public final class FallbackJSBundleLoader extends JSBundleLoader {
+
+ /* package */ static final String RECOVERABLE = "facebook::react::Recoverable";
+
+ // Loaders to delegate to, with the preferred one at the top.
+ private Stack<JSBundleLoader> mLoaders;
+
+ // Reasons why we fell-back on previous loaders, in order of occurrence.
+ private final ArrayList<Exception> mRecoveredErrors = new ArrayList<>();
+
+ /**
+ * @param loaders Loaders for the sources to try, in descending order of
+ * preference.
+ */
+ public FallbackJSBundleLoader(List<JSBundleLoader> loaders) {
+ mLoaders = new Stack();
+ ListIterator<JSBundleLoader> it = loaders.listIterator(loaders.size());
+ while (it.hasPrevious()) {
+ mLoaders.push(it.previous());
+ }
+ }
+
+ /**
+ * This loader delegates to (and so behaves like) the currently preferred
+ * loader. If that loader fails in a recoverable way and we fall back from it,
+ * it is replaced by the next most preferred loader.
+ */
+ @Override
+ public String loadScript(CatalystInstanceImpl instance) {
+ while (true) {
+ try {
+ return getDelegateLoader().loadScript(instance);
+ } catch (Exception e) {
+ if (!e.getMessage().startsWith(RECOVERABLE)) {
+ throw e;
+ }
+
+ mLoaders.pop();
+ mRecoveredErrors.add(e);
+ // TODO (t14839302): Report a soft error for each swallowed exception.
+ }
+ }
+ }
+
+ private JSBundleLoader getDelegateLoader() {
+ if (!mLoaders.empty()) {
+ return mLoaders.peek();
+ }
+
+ RuntimeException fallbackException =
+ new RuntimeException("No fallback options available");
+
+ // Invariant: tail.getCause() == null
+ Throwable tail = fallbackException;
+ for (Exception e : mRecoveredErrors) {
+ tail.initCause(e);
+ while (tail.getCause() != null) {
+ tail = tail.getCause();
+ }
+ }
+
+ throw fallbackException;
+ }
+}
@@ -0,0 +1,18 @@
+include_defs('//ReactAndroid/DEFS')
+
+rn_robolectric_test(
+ name = 'cxxbridge',
+ # Please change the contact to the oncall of your team
+ contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'],
+ srcs = glob(['*Test.java']),
+ deps = [
+ react_native_dep('third-party/java/fest:fest'),
+ react_native_dep('third-party/java/junit:junit'),
+ react_native_dep('third-party/java/mockito:mockito'),
+ react_native_dep('third-party/java/robolectric3/robolectric:robolectric'),
+ react_native_target('java/com/facebook/react/cxxbridge:bridge'),
+ ],
+ visibility = [
+ 'PUBLIC'
+ ],
+)
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.cxxbridge;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.fail;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FallbackJSBundleLoaderTest {
+
+ private static final String UNRECOVERABLE;
+ static {
+ String prefix = FallbackJSBundleLoader.RECOVERABLE;
+ char first = prefix.charAt(0);
+
+ UNRECOVERABLE = prefix.replace(first, (char) (first + 1));
+ }
+
+ @Test
+ public void firstLoaderSucceeds() {
+ JSBundleLoader delegates[] = new JSBundleLoader[] {
+ successfulLoader("url1"),
+ successfulLoader("url2")
+ };
+
+ FallbackJSBundleLoader fallbackLoader =
+ new FallbackJSBundleLoader(new ArrayList<>(Arrays.asList(delegates)));
+
+ assertThat(fallbackLoader.loadScript(null)).isEqualTo("url1");
+
+ verify(delegates[0], times(1)).loadScript(null);
+ verify(delegates[1], never()).loadScript(null);
+ }
+
+ @Test
+ public void fallingBackSuccessfully() {
+ JSBundleLoader delegates[] = new JSBundleLoader[] {
+ recoverableLoader("url1", "error1"),
+ successfulLoader("url2"),
+ successfulLoader("url3")
+ };
+
+ FallbackJSBundleLoader fallbackLoader =
+ new FallbackJSBundleLoader(new ArrayList<>(Arrays.asList(delegates)));
+
+ assertThat(fallbackLoader.loadScript(null)).isEqualTo("url2");
+
+ verify(delegates[0], times(1)).loadScript(null);
+ verify(delegates[1], times(1)).loadScript(null);
+ verify(delegates[2], never()).loadScript(null);
+ }
+
+ @Test
+ public void fallingbackUnsuccessfully() {
+ JSBundleLoader delegates[] = new JSBundleLoader[] {
+ recoverableLoader("url1", "error1"),
+ recoverableLoader("url2", "error2")
+ };
+
+ FallbackJSBundleLoader fallbackLoader =
+ new FallbackJSBundleLoader(new ArrayList<>(Arrays.asList(delegates)));
+
+ try {
+ fallbackLoader.loadScript(null);
+ fail("expect throw");
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(RuntimeException.class);
+
+ Throwable cause = e.getCause();
+ ArrayList<String> msgs = new ArrayList<>();
+ while (cause != null) {
+ msgs.add(cause.getMessage());
+ cause = cause.getCause();
+ }
+
+ assertThat(msgs).containsExactly(
+ recoverableMsg("error1"),
+ recoverableMsg("error2"));
+ }
+
+ verify(delegates[0], times(1)).loadScript(null);
+ verify(delegates[1], times(1)).loadScript(null);
+ }
+
+ @Test
+ public void unrecoverable() {
+ JSBundleLoader delegates[] = new JSBundleLoader[] {
+ fatalLoader("url1", "error1"),
+ recoverableLoader("url2", "error2")
+ };
+
+ FallbackJSBundleLoader fallbackLoader =
+ new FallbackJSBundleLoader(new ArrayList(Arrays.asList(delegates)));
+
+ try {
+ fallbackLoader.loadScript(null);
+ fail("expect throw");
+ } catch (Exception e) {
+ assertThat(e.getMessage()).isEqualTo(fatalMsg("error1"));
+ }
+
+ verify(delegates[0], times(1)).loadScript(null);
+ verify(delegates[1], never()).loadScript(null);
+ }
+
+ private static JSBundleLoader successfulLoader(String url) {
+ JSBundleLoader loader = mock(JSBundleLoader.class);
+ when(loader.loadScript(null)).thenReturn(url);
+
+ return loader;
+ }
+
+ private static String recoverableMsg(String errMsg) {
+ return FallbackJSBundleLoader.RECOVERABLE + errMsg;
+ }
+
+ private static JSBundleLoader recoverableLoader(String url, String errMsg) {
+ JSBundleLoader loader = mock(JSBundleLoader.class);
+ when(loader.loadScript(null))
+ .thenThrow(new RuntimeException(FallbackJSBundleLoader.RECOVERABLE + errMsg));
+
+ return loader;
+ }
+
+ private static String fatalMsg(String errMsg) {
+ return UNRECOVERABLE + errMsg;
+ }
+
+ private static JSBundleLoader fatalLoader(String url, String errMsg) {
+ JSBundleLoader loader = mock(JSBundleLoader.class);
+ when(loader.loadScript(null))
+ .thenThrow(new RuntimeException(UNRECOVERABLE + errMsg));
+
+ return loader;
+ }
+}

0 comments on commit c3892fa

Please sign in to comment.