0 ? $timeout : self::FALLBACK_TTL; } /** * 生成一个持有者标识。 * * @return string */ public static function make_owner() { return get_current_user_id() . '-' . wp_generate_password( 12, false, false ); } /** * 抢锁。 * * @param string $key 锁键,见 key()。 * @param string $owner 持有者标识。 * @return bool 是否抢到。 */ public static function acquire( $key, $owner ) { $payload = self::payload( $owner ); // 键不存在时才会插入成功——这一步是原子的。 if ( add_option( 'apgt_lock_' . self::key_hash( $key ), $payload, '', 'no' ) ) { self::release_on_shutdown( $key, $owner ); return true; } $current = self::info( $key ); if ( ! $current || ( time() - (int) $current['time'] ) > self::ttl() ) { // 僵尸锁,抢过来。极端并发下可能有两个进程同时判定超时, // 后果只是一个页面被翻译两次(译文按 md5 upsert,不会产生脏数据)。 update_option( 'apgt_lock_' . self::key_hash( $key ), $payload, 'no' ); self::release_on_shutdown( $key, $owner ); return true; } return false; } /** * 注册兜底释放:脚本无论怎么结束都把自己的锁还回去。 * * 正常流程会主动 release(),这里管的是异常退出——PHP 超时、致命错误、 * 或者进程被回收。不然这把锁要等满超时时间才轮得到下一个人,中间 * 该页面的翻译请求都白跑。release() 会核对持有者,重复调用不会误删别人的锁。 * * @param string $key 锁键。 * @param string $owner 持有者标识。 * @return void */ private static function release_on_shutdown( $key, $owner ) { if ( ! function_exists( 'register_shutdown_function' ) ) { return; } register_shutdown_function( function () use ( $key, $owner ) { self::release( $key, $owner ); } ); } /** * 让锁跟随翻译进度自动续期。 * * 一次整页翻译由多次 AI 请求组成,总耗时可以远超单次请求的超时; * 挂到 apgt_ai_request_done 上(APGT_Translator 每完成一次 AI 请求触发), * 只要任务还活着锁龄就不会超过一次请求的时长,不会被人当僵尸抢走。 * * @param string $key 锁键。 * @param string $owner 持有者标识。 * @return void */ public static function keep_alive( $key, $owner ) { add_action( 'apgt_ai_request_done', function () use ( $key, $owner ) { self::touch( $key, $owner ); } ); } /** * 续期,只有持有者能续。 * * @param string $key 锁键。 * @param string $owner 持有者标识。 * @return void */ public static function touch( $key, $owner ) { $current = self::info( $key ); if ( $current && $current['owner'] === $owner ) { update_option( 'apgt_lock_' . self::key_hash( $key ), self::payload( $owner ), 'no' ); } } /** * 释放锁,只有持有者能释放。 * * @param string $key 锁键。 * @param string $owner 持有者标识。 * @return void */ public static function release( $key, $owner ) { $current = self::info( $key ); if ( ! $current || $current['owner'] === $owner ) { delete_option( 'apgt_lock_' . self::key_hash( $key ) ); } } /** * 读当前锁信息。 * * @param string $key 锁键。 * @return array|null [ 'owner'=>string, 'time'=>int, 'user'=>int ] */ public static function info( $key ) { $raw = get_option( 'apgt_lock_' . self::key_hash( $key ) ); if ( ! $raw ) { return null; } $data = json_decode( (string) $raw, true ); if ( ! is_array( $data ) || ! isset( $data['owner'], $data['time'] ) ) { return null; } return $data; } /** * 锁当前是否被别人持有(用于给界面提示)。 * * @param string $key 锁键。 * @return string 持有者的显示名;无人持有时返回空串。 */ public static function holder_name( $key ) { $info = self::info( $key ); if ( ! $info || ( time() - (int) $info['time'] ) > self::ttl() ) { return ''; } $user = get_userdata( (int) $info['user'] ); return $user ? $user->display_name : __( 'another process', 'aipogeo-context-translate' ); } /** * 锁键的散列,接在 option 名前缀之后。 * * 键里含页面路径,长度没谱,md5 之后既定长又只剩安全字符。 * * option 名的 apgt_lock_ 前缀写在各个调用点上、且是字面量:option 名 * 必须带插件前缀,而检查这件事的静态扫描只认调用处第一个参数里的 * 字面文本,藏进变量或常量都会被判成「未加前缀」。 * * @param string $key 锁键。 * @return string */ private static function key_hash( $key ) { return md5( (string) $key ); } /** * 组装锁内容。 * * @param string $owner 持有者标识。 * @return string */ private static function payload( $owner ) { return wp_json_encode( array( 'owner' => (string) $owner, 'user' => get_current_user_id(), 'time' => time(), ) ); } }