0.0.8:卡片同步尝试用短时任务延长窗口(10 秒 → 最多 60 秒)
背景:上一步实现了"卡片 30 分钟刷新驱动同步",但受 FormExtensionAbility "回调后进程仅存续 10 秒"的硬限制,同步可能被回收掐断。 用户建议:能不能调一个后台进程、或让异步同步在 10 秒后继续跑一会儿? 调研结论(已核实官方文档): 1. ❌ ServiceExtensionAbility(独立后台服务)—— 三方应用**根本不能用**: 官方"仅系统应用可使用",需 Full SDK + AllowAppUsePrivilegeExtension 特权 + 设备白名单。 2. ❌ 从卡片进程 startAbility 拉起主应用 —— 会被拦:官方《组件启动规则》 "位于后台的 UIAbility 应用,启动组件需校验 ohos.permission.START_ABILITIES_FROM_BACKGROUND, **该权限仅系统应用可申请**"。 (卡片 postCardAction 的 `call` 事件确实能"后台拉起 UIAbility 且不弹界面", 但**只能由用户点击卡片触发**,系统定时刷新的 onUpdateForm 用不上。) 3. ✅ **短时任务 `requestSuspendDelay`** —— 可行路径:让进程在后台多活一会儿, 单次最长 3 分钟(低电量 1 分钟),实际时长以 actualDelayTime 为准。 本次改动(BackgroundSyncRunner): - 新增 `acquireCardWindow()`:尝试申请短时任务, · **容错尝试**:官方规定应在"应用在前台"或"onBackground 回调"里调用, 卡片进程是否符合**未知** → 抛错 / 时长为 0 时**优雅退回 9 秒软超时**,不阻塞流程; · 留 **15 秒余量**提前收手(保证能干净地 cancelSuspendDelay, 否则超时不取消会被系统强制终止进程并浪费配额); · 加 **60 秒/次总上限**(CARD_MAX_TOTAL_MS)。 - 新增 `releaseCardWindow()`:无论成败都在 finally 里释放,归还配额。 - `runOnceThrottledByCard()` 改为按"实际可用窗口"设软超时,并记录窗口秒数到日志。 ⚠️ 配额账(关键取舍): 短时任务**单日总配额仅 10 分钟**,且**只在被挂起后计时**(10 秒内跑完不计费)。 · 正常网络下增量同步几秒完成 → 多落在 10 秒窗口内 → 消耗极少; · 网络慢才会吃配额;吃满 10 分钟后自动降级为 9 秒软超时,不报错; · ⚠️ **不能吃光配额**:EntryAbility.onBackground 也申请短时任务(兜"退后台那一次同步"), 故卡片侧设 60 秒/次上限 + 15 秒提前释放,避免把它饿死。 验证:编译通过(BUILD SUCCESSFUL);窗口预算逻辑离线用例通过 (申请被拒/时长为0 → 9 秒;低电量 → 45 秒;正常 3 分钟 → 被 60 秒上限截断;40 秒 → 25 秒)。
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
//
|
//
|
||||||
// ⚠️ 与 SyncEngine.syncAccount 的分工:本类不关心超时与互斥锁(由 SyncEngine 内部处理)。
|
// ⚠️ 与 SyncEngine.syncAccount 的分工:本类不关心超时与互斥锁(由 SyncEngine 内部处理)。
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { DavAccount, AccountStore, TYPE_CALDAV } from './AccountStore';
|
import { DavAccount, AccountStore, TYPE_CALDAV } from './AccountStore';
|
||||||
import { SyncEngine } from './SyncEngine';
|
import { SyncEngine } from './SyncEngine';
|
||||||
@@ -35,12 +36,26 @@ export class BackgroundSyncRunner {
|
|||||||
/** 距上次同步不足该时长则跳过(略小于卡片周期,避免系统刷新漂移导致被误跳过) */
|
/** 距上次同步不足该时长则跳过(略小于卡片周期,避免系统刷新漂移导致被误跳过) */
|
||||||
private static readonly CARD_MIN_GAP_MS: number = 25 * 60 * 1000;
|
private static readonly CARD_MIN_GAP_MS: number = 25 * 60 * 1000;
|
||||||
/**
|
/**
|
||||||
* 卡片进程的软超时:官方规定 FormExtensionAbility 生命周期回调后进程**仅存续 10 秒**,
|
* 卡片进程的软超时(**未申请到短时任务时**):官方规定 FormExtensionAbility
|
||||||
* 超过就会被回收(异步任务被强制中止)。这里主动在 9 秒收手,
|
* 生命周期回调后进程**仅存续 10 秒**,超过就被回收(异步任务被强制中止)。
|
||||||
* 让"这一次同步"**干净地放弃**而不是被硬杀在半途 —— 幂等同步中断本就不损坏数据,
|
* 这里主动在 9 秒收手,让"这一次同步"**干净地放弃**而不是被硬杀在半途。
|
||||||
* 主动放弃还能保住"时间戳已写入、下轮再试"的一致性。
|
|
||||||
*/
|
*/
|
||||||
private static readonly CARD_SOFT_TIMEOUT_MS: number = 9 * 1000;
|
private static readonly CARD_SOFT_TIMEOUT_MS: number = 9 * 1000;
|
||||||
|
/**
|
||||||
|
* 申请到短时任务后的软超时:短时任务单次最长 3 分钟(低电量 1 分钟),
|
||||||
|
* 但实际时长以 `actualDelayTime` 为准 → 留 15 秒余量提前收手,保证能干净地
|
||||||
|
* `cancelSuspendDelay`(否则超时不取消会被系统强制终止进程、并浪费当日配额)。
|
||||||
|
*/
|
||||||
|
private static readonly CARD_EXT_SOFT_RESERVE_MS: number = 15 * 1000;
|
||||||
|
/**
|
||||||
|
* 卡片驱动同步可用的**总时长上限**:即使申请到短时任务也不超过这个值。
|
||||||
|
* 理由:短时任务**单日总配额仅 10 分钟**,30 分钟一次的话每次平均只能分到约 20 秒,
|
||||||
|
* 全用光会让"退后台那一次同步"(EntryAbility.onBackground 申请)无配额可用。
|
||||||
|
*/
|
||||||
|
private static readonly CARD_MAX_TOTAL_MS: number = 60 * 1000;
|
||||||
|
|
||||||
|
/** 本次卡片同步申请的短时任务 requestId(0 = 未申请到) */
|
||||||
|
private static cardTransientId: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ⭐⭐ 由**服务卡片定时刷新**(`onUpdateForm`)驱动的后台同步(0.0.8)。
|
* ⭐⭐ 由**服务卡片定时刷新**(`onUpdateForm`)驱动的后台同步(0.0.8)。
|
||||||
@@ -66,25 +81,82 @@ export class BackgroundSyncRunner {
|
|||||||
}
|
}
|
||||||
// 先写时间戳:避免同步执行期间卡片再次刷新导致并发重入
|
// 先写时间戳:避免同步执行期间卡片再次刷新导致并发重入
|
||||||
await AppSettings.setCardSyncAt(context, now);
|
await AppSettings.setCardSyncAt(context, now);
|
||||||
LogUtil.write('卡片驱动同步:开始(卡片 30 分钟定时刷新触发)');
|
// ⭐ 尝试申请**短时任务**,把 10 秒窗口延长到最多 3 分钟。
|
||||||
// 软超时:9 秒内没跑完就主动放弃(进程本来也只有 10 秒窗口)
|
// ⚠️ 官方规定 requestSuspendDelay 应在"应用处于前台"或"onBackground 回调"里调用;
|
||||||
|
// 卡片进程(FormExtensionAbility)是否符合**未知** → 故此处**容错尝试**:
|
||||||
|
// 成功则按实际时长延长,失败(抛错/时长为 0)则退回 9 秒软超时。
|
||||||
|
// ⚠️ 配额:短时任务单日总配额 10 分钟、单次最长 3 分钟(低电量 1 分钟),
|
||||||
|
// 且**被挂起后才计时**(10 秒内跑完不消耗)→ 对我们很划算。
|
||||||
|
const windowMs: number = await BackgroundSyncRunner.acquireCardWindow();
|
||||||
|
const budget: number = Math.min(windowMs, BackgroundSyncRunner.CARD_MAX_TOTAL_MS);
|
||||||
|
LogUtil.write(`卡片驱动同步:开始(卡片 30 分钟定时刷新触发,可用窗口 ${Math.round(budget / 1000)} 秒)`);
|
||||||
|
// 软超时:预算内没跑完就主动放弃(幂等同步,中断不损坏数据,下轮会重试)
|
||||||
const worker: Promise<void> =
|
const worker: Promise<void> =
|
||||||
BackgroundSyncRunner.runOnce(context, 'cardTick').then((): void => {});
|
BackgroundSyncRunner.runOnce(context, 'cardTick').then((): void => {});
|
||||||
const guard: Promise<void> = new Promise<void>((_res: () => void, rej: (e: Error) => void) => {
|
const guard: Promise<void> = new Promise<void>((_res: () => void, rej: (e: Error) => void) => {
|
||||||
setTimeout((): void => rej(new Error('卡片进程窗口不足,本次同步放弃')),
|
setTimeout((): void => rej(new Error('本次同步时间预算用尽,主动收手')),
|
||||||
BackgroundSyncRunner.CARD_SOFT_TIMEOUT_MS);
|
budget);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await Promise.race<void>([worker, guard]);
|
await Promise.race<void>([worker, guard]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as Error;
|
const e = err as Error;
|
||||||
LogUtil.write(`卡片驱动同步提前收手:${e.message}(幂等,下轮会重试)`);
|
LogUtil.write(`卡片驱动同步提前收手:${e.message}(幂等,下轮会重试)`);
|
||||||
|
} finally {
|
||||||
|
// 无论成败都释放短时任务:① 归还当日配额 ② 避免超时不取消被系统强杀进程
|
||||||
|
BackgroundSyncRunner.releaseCardWindow();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const e = err as BusinessError;
|
const e = err as BusinessError;
|
||||||
LogUtil.write(`卡片驱动同步异常: ${e.message}`);
|
LogUtil.write(`卡片驱动同步异常: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为卡片同步申请短时任务,返回**实际可用窗口(毫秒)**。
|
||||||
|
* 申请失败/异常/时长为 0 → 返回 0,由调用方退回默认的 9 秒软超时(不阻塞流程)。
|
||||||
|
*/
|
||||||
|
private static async acquireCardWindow(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const info: backgroundTaskManager.DelaySuspendInfo =
|
||||||
|
backgroundTaskManager.requestSuspendDelay('正在同步日历数据', (): void => {
|
||||||
|
// 超时回调:配额即将用尽,清掉本地记录(系统会自行处理挂起)
|
||||||
|
LogUtil.write('卡片同步的短时任务即将超时');
|
||||||
|
BackgroundSyncRunner.cardTransientId = 0;
|
||||||
|
});
|
||||||
|
BackgroundSyncRunner.cardTransientId = info.requestId;
|
||||||
|
const actual: number = info.actualDelayTime ?? 0;
|
||||||
|
if (actual <= 0) {
|
||||||
|
// 拿到了 requestId 但没有实际时长 → 视为不可用
|
||||||
|
BackgroundSyncRunner.releaseCardWindow();
|
||||||
|
LogUtil.write('卡片同步:短时任务时长为 0,退回 9 秒软超时');
|
||||||
|
return BackgroundSyncRunner.CARD_SOFT_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
// 留 15 秒余量提前收手,保证能干净地取消
|
||||||
|
const usable: number = Math.max(actual - BackgroundSyncRunner.CARD_EXT_SOFT_RESERVE_MS,
|
||||||
|
BackgroundSyncRunner.CARD_SOFT_TIMEOUT_MS);
|
||||||
|
LogUtil.write(`卡片同步:短时任务已获批,实际时长 ${Math.round(actual / 1000)} 秒`);
|
||||||
|
return usable;
|
||||||
|
} catch (err) {
|
||||||
|
// 例如"未在前台/退后台时机申请"被拒 —— 退回 9 秒软超时,不影响流程
|
||||||
|
const e = err as BusinessError;
|
||||||
|
LogUtil.write(`卡片同步:短时任务申请失败(${e.code ?? ''} ${e.message}),退回 9 秒软超时`);
|
||||||
|
return BackgroundSyncRunner.CARD_SOFT_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 释放卡片同步的短时任务(幂等) */
|
||||||
|
private static releaseCardWindow(): void {
|
||||||
|
if (BackgroundSyncRunner.cardTransientId === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
backgroundTaskManager.cancelSuspendDelay(BackgroundSyncRunner.cardTransientId);
|
||||||
|
} catch (err) {
|
||||||
|
// 取消失败无需处理
|
||||||
|
}
|
||||||
|
BackgroundSyncRunner.cardTransientId = 0;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 跑一次完整同步。全流程 try-catch,**永不抛错**(后台路径没有 UI 承担异常)。
|
* 跑一次完整同步。全流程 try-catch,**永不抛错**(后台路径没有 UI 承担异常)。
|
||||||
* @param context 应用上下文
|
* @param context 应用上下文
|
||||||
|
|||||||
Reference in New Issue
Block a user