Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file modified android/lib/arm64-v8a/librnpushy.so
Binary file not shown.
Binary file modified android/lib/armeabi-v7a/librnpushy.so
Binary file not shown.
Binary file modified android/lib/x86/librnpushy.so
Binary file not shown.
Binary file modified android/lib/x86_64/librnpushy.so
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ private FlowBridge() {
*/
static native String nextQueryStep(String stateJson, String responseText);

/**
* One step of the round driver (cpp/update_flow_core/round_engine.h):
* {"state", "commands"} for the previous state (null to start) and the
* event. Null only when the core threw.
*/
static native String stepRound(String stateJson, String eventJson);

/** The decision with planned attempts, hashInfo and the response-cache text. */
static native String handleResponse(
String responseText, String configJson, String envJson, String requestBody,
Expand Down
470 changes: 208 additions & 262 deletions android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,42 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.json.JSONArray;
import java.util.concurrent.TimeUnit;
import org.json.JSONObject;
import org.junit.Test;

// The round's policy (order, mirror retry, budgets, commit and result
// mapping) is the shared engine's, pinned by
// cpp/update_flow_core/tests/round-engine-vectors.json. These cover what the
// Android adapter adds: running one download command and reporting it back.
public class SyncCoordinatorTest {
/** Settles every attempt synchronously from a per-url script. */
/** Settles (or, with no outcome, never settles) each attempt synchronously. */
private static final class ScriptedStarter implements SyncCoordinator.DownloadStarter {
final List<String> started = new ArrayList<>();
private final java.util.Map<String, Throwable> failures = new java.util.HashMap<>();
private final Throwable error;
private final boolean settles;
boolean cancelled;

ScriptedStarter fail(String url, Throwable error) {
failures.put(url, error);
return this;
ScriptedStarter(Throwable error, boolean settles) {
this.error = error;
this.settles = settles;
}

@Override
public DownloadTaskParams start(
String type, String url, UpdateContext.DownloadFileListener listener, long deadlineNanos
String strategy, String url, String hash, String originHash,
UpdateContext.DownloadFileListener listener, long deadlineNanos
) {
started.add(type + " " + url);
Throwable error = failures.get(url);
started.add(strategy + " " + url + " " + hash + " " + originHash);
if (!settles) {
return new DownloadTaskParams() {
@Override
void cancel() {
cancelled = true;
}
};
}
if (error != null) {
listener.onDownloadFailed(error);
} else {
Expand All @@ -38,59 +51,56 @@ public DownloadTaskParams start(
}
}

private static JSONArray attempts(Object[]... attempts) throws Exception {
JSONArray result = new JSONArray();
for (Object[] attempt : attempts) {
JSONObject entry = new JSONObject();
entry.put("type", attempt[0]);
entry.put("urls", new JSONArray(Arrays.asList(attempt).subList(1, attempt.length)));
result.put(entry);
private static JSONObject command(String strategy, long budgetMillis) throws Exception {
JSONObject command = new JSONObject()
.put("type", "download")
.put("strategy", strategy)
.put("url", "https://a/" + strategy)
.put("hash", "h2")
.put("deadline", SyncCoordinator.engineMillis(
System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(budgetMillis)));
if ("diff".equals(strategy)) {
command.put("originHash", "cur");
}
return result;
return command;
}

private static PatchFailedException patchFailed() {
return new PatchFailedException("hpatch failed", null);
@Test
public void aSettledAttemptReportsOk() throws Exception {
ScriptedStarter starter = new ScriptedStarter(null, true);
JSONObject event = SyncCoordinator.download(command("diff", 10_000), starter);

assertTrue(event.getBoolean("ok"));
assertFalse(event.has("code"));
assertEquals("diff https://a/diff h2 cur", starter.started.get(0));
}

@Test
public void patchFailureSkipsTheStrategysRemainingMirrors() throws Exception {
ScriptedStarter starter = new ScriptedStarter()
.fail("https://a/diff", patchFailed())
.fail("https://a/full", new IOException("reset"));
boolean downloaded = SyncCoordinator.performAttempts(
attempts(
new Object[] {"diff", "https://a/diff", "https://b/diff"},
new Object[] {"full", "https://a/full", "https://b/full"}),
0, starter);
public void aFailureReportsItsErrorCode() throws Exception {
JSONObject patch = SyncCoordinator.download(command("pdiff", 10_000),
new ScriptedStarter(new PatchFailedException("hpatch failed", null), true));
JSONObject network = SyncCoordinator.download(command("full", 10_000),
new ScriptedStarter(new IOException("reset"), true));

assertTrue(downloaded);
assertEquals(
Arrays.asList("diff https://a/diff", "full https://a/full", "full https://b/full"),
starter.started);
assertFalse(patch.getBoolean("ok"));
assertEquals("PATCH_FAILED", patch.getString("code"));
assertEquals("DOWNLOAD_FAILED", network.getString("code"));
}

@Test
public void retryableFailuresTryEveryMirror() throws Exception {
ScriptedStarter starter = new ScriptedStarter()
.fail("https://a/full", new IOException("timeout"))
.fail("https://b/full", new FileOperationException("refused"));
boolean downloaded = SyncCoordinator.performAttempts(
attempts(new Object[] {"full", "https://a/full", "https://b/full"}),
0, starter);
public void anAttemptPastItsDeadlineIsCancelledAndTimedOut() throws Exception {
ScriptedStarter starter = new ScriptedStarter(null, false);
JSONObject event = SyncCoordinator.download(command("full", 50), starter);

assertFalse(downloaded);
assertEquals(
Arrays.asList("full https://a/full", "full https://b/full"), starter.started);
assertTrue(event.getBoolean("timedOut"));
assertTrue(starter.cancelled);
assertEquals("full https://a/full h2 null", starter.started.get(0));
}

// Attempt planning (diff needs a running version, https-only rounds drop
// cleartext URLs) and the https-only rule live in the shared core:
// cpp/update_flow_core/tests/native-round-vectors.json.

@Test
public void onlyPatchFailuresAreNotMirrorRetryable() {
assertEquals("PATCH_FAILED", ErrorCodes.forDownloadFailure(patchFailed()));
assertEquals("PATCH_FAILED",
ErrorCodes.forDownloadFailure(new PatchFailedException("x", null)));
assertEquals("FILE_OPERATION_FAILED",
ErrorCodes.forDownloadFailure(new FileOperationException("x")));
assertEquals("DOWNLOAD_FAILED", ErrorCodes.forDownloadFailure(new IOException("x")));
Expand Down
1 change: 1 addition & 0 deletions cpp/android_jni/jni_natives.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobjectArray ApplyArchiveDelta(JNIEnv* env, jclass, jint patch_type, jobjectArra
void CleanupOldEntries(JNIEnv* env, jclass, jstring root_dir, jobjectArray keep_names, jint max_age_days, jstring trash_dir);
jstring FlowPrepareRound(JNIEnv* env, jclass, jstring configJson, jstring envJson, jdouble randomSample);
jstring FlowNextQueryStep(JNIEnv* env, jclass, jstring stateJson, jstring responseText);
jstring FlowStepRound(JNIEnv* env, jclass, jstring stateJson, jstring eventJson);
jstring FlowHandleResponse(JNIEnv* env, jclass, jstring responseText, jstring configJson, jstring envJson, jstring requestBody, jlong responseAtSeconds);
jboolean FlowConfigsEquivalent(JNIEnv* env, jclass, jstring a, jstring b);
jstring FlowRunnableConfig(JNIEnv* env, jclass, jstring configJson);
Expand Down
2 changes: 2 additions & 0 deletions cpp/android_jni/jni_registration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const std::vector<NativeClass>& NativeClasses() {
reinterpret_cast<void*>(&pushy::jni_natives::FlowPrepareRound)},
{"3412ecc59f9e6d573b0c08fcc6", "723bfed0b88a274923311bb6e5a7826444205fcdf4daae943d432d07e18c93a9887e5a3655a2e4af83897d163a12fecae5b4705357351fae",
reinterpret_cast<void*>(&pushy::jni_natives::FlowNextQueryStep)},
{"2903f1c19c847d4b26", "723bfed0b88a274923311bb6e5a7826444205fcdf4daae943d432d07e18c93a9887e5a3655a2e4af83897d163a12fecae5b4705357351fae",
reinterpret_cast<void*>(&pushy::jni_natives::FlowStepRound)},
{"3216fad5a28e5a40312f13f7c5b6", "723bfed0b88a274923311bb6e5a7826444205fcdf4daae943d432d07e18c93a9887e5a3655c7c2a4949e3355371df78299937648503c43d9d8ae9a68092f0113fd9887857c422602b9d595959c72462c45ebc5afb9d44b412006e2cefd",
reinterpret_cast<void*>(&pushy::jni_natives::FlowHandleResponse)},
{"3918fad7a78c7b60332a15efd7bf95635e", "723bfed0b88a274923311bb6e5a7826444205fcdf4daae943d432d07e18c93a9887e5a3655a2f2",
Expand Down
17 changes: 17 additions & 0 deletions cpp/android_jni/update_flow_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "../update_flow_core/flow_json.h"
#include "../update_flow_core/native_config.h"
#include "../update_flow_core/native_round.h"
#include "../update_flow_core/round_engine.h"

using pushy::jni_util::ToJString;

Expand Down Expand Up @@ -50,6 +51,22 @@ jstring pushy::jni_natives::FlowPrepareRound(JNIEnv* env, jclass,
}
}

// The round driver (update_flow_core/round_engine.h). A null state starts
// fresh; input that does not parse finishes the round inside the core, so
// only an exception yields null.
jstring pushy::jni_natives::FlowStepRound(JNIEnv* env, jclass,
jstring stateJson,
jstring eventJson) {
try {
return ToJString(
env, flowcore::StepRoundJson(
pushy::jni_util::JStringToString(env, stateJson),
pushy::jni_util::JStringToString(env, eventJson)));
} catch (...) {
return nullptr;
}
}

// A null responseText is a failed request (or none made yet).
jstring pushy::jni_natives::FlowNextQueryStep(JNIEnv* env, jclass,
jstring stateJson,
Expand Down
5 changes: 3 additions & 2 deletions docs/NATIVE_CHECKUPDATE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ full 超时整体失败)、激活判定(`activate || crashHold || forceActiv
由 `tests/round-engine-vectors.json` 锁定,接入各端时随之生效:

- 宿主取消判定:鸿蒙原用字符串全等比较配置,改为 C1 等价(与 Android/iOS 一致)。
- 提交带配置代数:iOS 早已在锁内复核;Android/鸿蒙接入时补上(原先只在宿主等待后检查)。
- 提交带配置代数:iOS 在锁内复核两个代数;Android 换配置时同时递增 reset 代数,提交天然被拒,适配器再按配置代数回报 `config_changed`;鸿蒙接入时对齐。
- HTTP 超时:三端统一为空闲 10s + 整次 15s,并按预算钳制(iOS 原先的信号量兜底会比预算多等 5s)。
- 下载超时:显式 `timedOut`,不再依赖鸿蒙把超时包装成 DOWNLOAD_FAILED 后由下一次循环看 deadline。
- 只提交缓存的提交失败统一为 `failed/commit_failed`(Android 原为 internal_error)。
Expand All @@ -645,4 +645,5 @@ full 超时整体失败)、激活判定(`activate || crashHold || forceActiv

进度:1、2 已合入(#673、#674)。iOS 已接入:`RCTPushyOrchestrator.mm` 的 `runOnce` 驱动引擎,
命令由 `perform:` / `download:` / `commit:` 执行,宿主取消判定改用 `HostCancellation`;提交败于配置
变更时由适配器按配置代数回报 `reason: "config_changed"`。
变更时由适配器按配置代数回报 `reason: "config_changed"`。Android 已接入:`SyncCoordinator.java` 的 `runOnce` 经 `FlowBridge.stepRound`(JNI)驱动引擎,
`download` 执行单次下载并回报 ok/code/timedOut(JVM 单测覆盖),`android/lib` 的 `.so` 随之重编。
2 changes: 1 addition & 1 deletion src/__tests__/jniRegistration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const registered = () => {
describe('JNI registration table', () => {
test('matches every Java native declaration', () => {
const natives = javaNatives();
expect(natives.length).toBe(18);
expect(natives.length).toBe(19);
expect(registered()).toEqual(natives);
});

Expand Down
Loading