From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master
---
jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/AuditSpoolWriter.java | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 178 insertions(+), 0 deletions(-)
diff --git a/jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/AuditSpoolWriter.java b/jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/AuditSpoolWriter.java
new file mode 100644
index 0000000..d465bd8
--- /dev/null
+++ b/jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/AuditSpoolWriter.java
@@ -0,0 +1,178 @@
+package jnpf.audit.sdk;
+
+import jnpf.util.JsonUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+import jnpf.audit.model.AuditEventDTO;
+
+/**
+ * 鏈湴鍏滃簳 spool 鍐欏叆鍣紙Task 7 Step 3锛夛細涓夌骇闄嶇骇鏈�鍚庝竴绾с��
+ *
+ * <p>鎸夋棩杩藉姞涓�琛� JSON 鍒� {@code ${audit.spool-dir}/audit-events-yyyyMMdd.jsonl}锛�
+ * IO 澶辫触浠� {@code log.error} + 涓㈠純璁℃暟锛�<b>缁濅笉鍚戜笟鍔℃姏寮傚父</b>鈥斺�斿璁¤惤鐩樺け璐ヤ笉鑳藉弽鍣笟鍔°��
+ *
+ * <p><b>骞跺彂瀹夊叏锛圕odex 鎵规浜屽楠� #1锛屽弻灞傞攣鍗忚缁堝眬淇锛�</b>锛歿@link #write} 鐨�
+ * "open(append)鈫掑啓涓�琛屸啋flush鈫抍lose" 鏁存鎼繘涓� {@link AuditSpoolReplayer} 鍏变韩鐨�
+ * {@link AuditSpoolLock} 涓寸晫鍖猴紙杩涚▼鍐� monitor + 璺ㄨ繘绋� {@code spool.lock} FileLock锛夈�傚洜姝�
+ * append 涓庨噸鏀惧櫒鐨�"璁ら rename"涓ユ牸浜掓枼鈥斺�斿交搴曟秷闄�"writer 缁忓凡鎵撳紑 FD 鍚戣鏀瑰悕 inode 鍐欏叆銆�
+ * 浜嬩欢闅� {@code .replaying} 琚垹鑰屾案涔呬涪澶�"鐨勭獥鍙g珵鎬侊紙鍘� 60 绉掗潤榛樼獥鍙d粎娲昏穬搴﹀惎鍙戝紡锛屼笉瓒充互淇濊瘉姝g‘鎬э紝
+ * 鐜伴檷绾т负绾紭鍖栵級銆倇@link Files#write} 鍗曟璋冪敤鍐呭嵆寮�-鍐�-鍏筹紙鍐欏叆閲� <0.5 QPS锛屾瘡娆″紑鍏虫祦鎬ц兘鏃犺櫈锛夛紝
+ * 閿佸唴鍙仛杩欎竴蹇搷浣滐紱閲嶆斁鍣ㄦ妸娲诲姩鏂囦欢 rename 璧板悗锛屼笅涓�娆� write 浠� {@link StandardOpenOption#CREATE}
+ * 鍦ㄥ師鍚嶆柊寤烘枃浠剁户缁拷鍔狅紝澶╃劧涓嶅啓鍥炴棫 inode锛堢粓楠屽洓杞憿锛夈��
+ *
+ * <p><b>闄嶇骇闅旂锛圕odex 鎵规浜屼笁杞� #A锛氭秷鐏叡浜悕锛屾潨缁濋檷绾ц矾寰� rename 绔炴�侊級</b>锛氳法杩涚▼ FileLock
+ * 涓�鏃舵嬁涓嶅埌锛堣秴鏃�/涓柇/涓嶅彲鐢�/宸查攢姣侊級鏃讹紝{@link AuditSpoolLock#lockAndRun(AuditSpoolLock.IoRunnable, AuditSpoolLock.IoRunnable)}
+ * 涓嶅啀璁╂湰娆″啓鍏ュ幓<b>杩藉姞鍏变韩娲诲姩鍚�</b>锛堥偅鏍蜂細涓庡叾瀹冭繘绋嬬殑"璁ら rename+鍒犻櫎"閲嶆紨 #1 涓簨浠舵椂搴忥細鏃犻攣
+ * open 娲诲姩鏂囦欢 鈫� 浠栬繘绋� rename+璇� 鈫� 鏈繘绋嬪悜琚敼鍚� inode 鍐� 鈫� 浠栬繘绋嬪垹 鈫� 浜嬩欢涓㈠け锛夛紝鑰屾槸鏀硅蛋
+ * "搴旀�ユ"璺緞锛氭妸璇ヨ鍐欏叆鍞竴鍚嶆枃浠讹紙涓�浜嬩欢涓�鏂囦欢锛屼粎闄嶇骇鍦烘櫙銆佷綆 QPS 鍙帴鍙楋級锛岀敱閲嶆斁鍣ㄩ攣鍐呰棰�
+ * 娑堣垂鈥斺�斿畞澶氬嚑涓枃浠朵篃涓嶄涪浜嬩欢銆傝繘绋嬪唴 monitor 鍏ㄧ▼鎸佹湁锛堝悓 JVM 涓茶涓嶅彉锛夈��
+ *
+ * <p><b>鍐欏悗鍘熷瓙鍙戝竷锛圕odex 鍥涜疆澶嶉獙 Critical#1锛屾秷鐏法杩涚▼鍗婂啓绐楀彛锛�</b>锛氫笂涓�鐗堟妸搴旀�ユ鐩存帴鐢ㄥ彲鎵弿鐨�
+ * {@code .retry} 鍚嶄竴姝� {@link StandardOpenOption#CREATE_NEW} 寤烘枃浠�+鍐欏叆鈥斺�斾絾"鐩綍椤瑰垱寤哄彲瑙�"涓�
+ * "鍐呭鍐欏畬 close"涔嬮棿瀛樺湪绐楀彛锛屽彟涓�杩涚▼鐨� {@link AuditSpoolReplayer} 鎸夋枃浠跺悕鎵弿/璁ら鍙兘钀借繘杩欎釜
+ * 绐楀彛锛氳鍒扮┖鏂囦欢 鈫� 鏀跺熬鎶婂畠褰撳鐞嗗畬鍒犳帀锛坱oken 涓㈠け锛夎�屾湰杩涚▼浠嶆寔宸叉墦寮� FD 鍚戝凡琚� unlink 鐨� inode
+ * 杩藉姞 鈫� 浜嬩欢闈欓粯涓㈠け锛涙垨璇诲埌鍗婅 鈫� 鍓嶇紑琚綋鍧忚闅旂杩� {@code .bad}锛岃鍐呭鏈笉瀹屾暣鍗磋鍒ゅ畾"宸插鐞�"銆�
+ * 鐜版敼涓�<b>鍏堝啓鍚庡彂甯�</b>涓ゆ锛宺ename 鐨勫師瀛愭�х墿鐞嗘秷鐏繖涓獥鍙o細
+ * <ol>
+ * <li>鍐欏叆鍞竴鍚� {@code <娲诲姩鍚�>.<UUID>.retry.writing}锛坽@link StandardOpenOption#CREATE_NEW}锛�
+ * {@link Files#write} 鍗曟璋冪敤鍐呭啓瀹� flush+close锛夆�斺�旀鏃舵枃浠跺悕涓嶅湪閲嶆斁鍣ㄦ壂鎻忛泦
+ * 锛坽@code .retry}/{@code .jsonl}锛夊唴锛屼换浣曡繘绋嬮兘涓嶄細璁ら瀹冿紱</li>
+ * <li>{@link Files#move} + {@link StandardCopyOption#ATOMIC_MOVE} 鍘熷瓙鏀瑰悕鍙戝竷涓�
+ * {@code <娲诲姩鍚�>.<UUID>.retry}鈥斺�攔ename 鏄枃浠剁郴缁熷師瀛愭搷浣滐紝{@code .retry} 杩欎釜鍙壂鎻忓悕
+ * 浠�"涓嶅瓨鍦�"鐩存帴璺冲埌"鍐呭瀹屾暣瀛樺湪"锛屼笉瀛樺湪涓棿鎬侊紝璺ㄨ繘绋嬭棰嗙獥鍙g墿鐞嗘秷澶便��</li>
+ * </ol>
+ * rename 鍥犳簮鏂囦欢宸蹭笉瀛樺湪鑰屽け璐ワ紙{@link NoSuchFileException}锛夋椂锛岃鏄庢湰娆″啓鍏ュ湪涓婇潰绗憼姝�
+ * {@link Files#write} 璋冪敤<b>鍐呴儴</b>琚樆濉炶繃涔咃紙纾佺洏/缃戠粶瀛樺偍 IO 鍗¢】锛夛紝瀵艰嚧
+ * {@link AuditSpoolReplayer#recoverOrphanedWriting()} 璇垽鍏惰秴榫勫苟鎶婃簮鏂囦欢闅旂涓� {@code .stale}鈥斺��
+ * 姝や负 Codex 浜旇疆淇鐨勮嚜鎰堝満鏅紝瑙佷笅涓�娈点�傚叾瀹� rename 澶辫触锛堢鐩樻弧銆佹潈闄愮獊鍙樼瓑鏋佺 IO 寮傚父锛夋椂浠嶅彧
+ * {@code log.error}锛堣劚鏁忥紝浠呰鏂囦欢鍚嶄笌 {@code clientEventId}锛屼笉璁颁簨浠舵鏂囷級銆�<b>淇濈暀 {@code .writing}
+ * 鏂囦欢涓嶅垹銆佷笉閲嶈瘯</b>鈥斺�斿唴瀹瑰凡瀹屾暣钀界洏锛屼笉璁″叆 {@link #discardCount}锛堜笉绠椾簨浠朵涪澶憋級锛涗氦鐢�
+ * {@link AuditSpoolReplayer} 鐨勫鍎垮洖鏀跺厹搴曪紙mtime 瓒呴槇鍊煎悗閿佸唴閲嶉獙+闅旂涓� {@code .stale}锛岀瓑寰呬汉宸�
+ * 鏍告煡琛ュ綍锛岃鍏剁被 Javadoc锛夈��
+ *
+ * <p><b>Codex 浜旇疆淇锛堣嚜鎰堥噸鍙戯細婧愯闅旂/鎸蛋鏃堕噸璇曚竴娆★級</b>锛歿@code .writing} 婧愭枃浠惰
+ * {@link AuditSpoolReplayer#recoverOrphanedWriting()} 闅旂涓� {@code .stale} 鍚庯紝鏈 rename 浼氭姏
+ * {@link NoSuchFileException}鈥斺�旀鏃朵簨浠跺唴瀹瑰苟鏈湡鐨勪涪澶憋紙宸插畬鏁磋惤鍦� {@code .stale} 閲岋紝绛夊緟浜哄伐
+ * 鏍告煡锛夛紝浣嗘湰娆℃姇閫掑皻鏈嚜鍔ㄥ畬鎴愩�備负浜嗕笉璁╁璁′簨浠朵粎浠呭洜涓轰竴娆℃瀬绔殑 IO 闃诲灏卞繀椤讳緷璧栦汉宸ヨˉ褰曪紝
+ * {@link #write} 鍦ㄦ鎯呭舰涓�<b>鍙噸璇曚竴娆�</b>锛氭崲涓�涓柊 {@code UUID} 閲嶆柊璧颁竴閬嶃�屽啓 {@code .writing}
+ * 鈫� move 鍙戝竷涓� {@code .retry}銆嶄袱姝ュ崗璁紙瑙� {@link #publishOnceOrRetryAfterQuarantine}锛夈�傞噸璇�
+ * 鎴愬姛鈥斺�斾簨浠跺凡姝e父鎶曢�掑埌閲嶆斁閫氶亾锛寋@code .stale} 閲岀殑鏃у壇鏈嚦澶氬湪浜哄伐鏍告煡琛ュ綍鏃堕�犳垚涓�娆¢噸澶嶏紝钀藉簱鎸�
+ * {@code clientEventId} 骞傜瓑鍚告敹锛屾棤瀹筹紱閲嶈瘯浠嶄互 {@code NoSuchFileException} 澶辫触锛堢悊璁轰笂鏋佺綍瑙侊紝闇�
+ * 杩炵画涓ゆ涓嶅悓 UUID 鐨勬枃浠堕兘琚殧绂伙級鍒欎笉鍐嶉噸璇曪紝{@code log.error}锛堝惈 {@code clientEventId}锛変氦鐢�
+ * 浜哄伐鏍告煡 {@code .stale} 闅旂鍖猴紝鍘熸牱淇濈暀鍏朵腑宸茬粡鍐欏ソ鐨勫畬鏁村唴瀹广��
+ */
+@Slf4j
+public class AuditSpoolWriter {
+
+ private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+ private final File spoolDir;
+ private final AuditSpoolLock spoolLock;
+ private final AtomicLong discardCount = new AtomicLong();
+
+ public AuditSpoolWriter(String spoolDir, AuditSpoolLock spoolLock) {
+ this.spoolDir = new File(spoolDir);
+ this.spoolLock = spoolLock;
+ }
+
+ public void write(AuditEventDTO event) {
+ try {
+ String fileName = "audit-events-" + LocalDate.now().format(DAY) + ".jsonl";
+ Path path = new File(spoolDir, fileName).toPath();
+ byte[] bytes = (JsonUtil.getObjectToString(event) + "\n") // jsonl 瑙勮寖鐢� \n
+ .getBytes(StandardCharsets.UTF_8);
+ // Codex 鎵规浜屽楠� #1锛氭寔閿佽矾寰� open-append-flush-close 鎼繘鍏变韩涓寸晫鍖猴紝涓� replayer 鐨勮棰� rename 浜掓枼
+ spoolLock.lockAndRun(
+ // lockedAction锛氭寔鏈夎法杩涚▼閿� 鈫� 姝e父杩藉姞鍏变韩娲诲姩鏂囦欢
+ () -> {
+ if (!spoolDir.exists()) {
+ Files.createDirectories(spoolDir.toPath());
+ }
+ Files.write(path, bytes,
+ StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+ },
+ // degradedAction锛圕odex 鎵规浜屼笁杞� #A + 鍥涜疆 Critical#1 + 浜旇疆鑷剤锛夛細鏈寔閿� 鈫� 鍐欏悗
+ // 鍘熷瓙鍙戝竷鍞竴鍚� .retry 搴旀�ユ锛屾秷鐏叡浜悕/鏉滅粷 rename 绔炴�併�佹秷鐏法杩涚▼鍗婂啓绐楀彛锛堣绫�
+ // Javadoc銆屽啓鍚庡師瀛愬彂甯冦�嶆锛夛紱鍙戝竷婧愯 AuditSpoolReplayer 瀛ゅ効闅旂鏃惰嚜鎰堥噸璇曚竴娆�
+ // 锛堣绫� Javadoc銆孋odex 浜旇疆淇锛堣嚜鎰堥噸鍙戯級銆嶆锛夈��
+ () -> {
+ if (!spoolDir.exists()) {
+ Files.createDirectories(spoolDir.toPath());
+ }
+ publishOnceOrRetryAfterQuarantine(fileName, bytes, event);
+ });
+ } catch (Throwable t) {
+ long n = discardCount.incrementAndGet();
+ log.error("AUDIT-SPOOL-LOST clientEventId={} 钀界洏澶辫触锛岀疮璁′涪寮�={}",
+ event == null ? null : event.getClientEventId(), n, t);
+ }
+ }
+
+ /**
+ * degradedAction 涓讳綋锛氬啓鍚庡師瀛愬彂甯冧袱姝ュ崗璁紙鍐欏敮涓�鍚� {@code .writing} 鈫� move 鍙戝竷涓�
+ * {@code .retry}锛夛紝婧愯瀛ゅ効闅旂锛坽@link NoSuchFileException}锛夋椂鑷剤<b>鍙噸璇曚竴娆�</b>鈥斺�旇绫�
+ * Javadoc銆屽啓鍚庡師瀛愬彂甯冦�嶄笌銆孋odex 浜旇疆淇锛堣嚜鎰堥噸鍙戯級銆嶄袱娈电殑瀹屾暣璁鸿瘉銆�
+ *
+ * @throws IOException 闈為殧绂诲師鍥犵殑 rename 澶辫触锛堢鐩樻弧绛夛級宸插湪鏂规硶鍐� log.error 鍚告敹骞惰繑鍥烇紝
+ * 浠� {@code Files.write} 鏈韩澶辫触锛堝纾佺洏宸叉弧瀵艰嚧鍐欎笉杩涘幓锛変細澶栨姏锛�
+ * 浜ょ敱 {@link #write} 鐨勫灞� catch 璁″叆 {@link #discardCount}
+ */
+ private void publishOnceOrRetryAfterQuarantine(String fileName, byte[] bytes, AuditEventDTO event)
+ throws IOException {
+ boolean retried = false;
+ while (true) {
+ String uuid = UUID.randomUUID().toString();
+ Path writing = new File(spoolDir, fileName + "." + uuid + ".retry.writing").toPath();
+ Path published = new File(spoolDir, fileName + "." + uuid + ".retry").toPath();
+ // 绗竴姝ワ細鍐欏敮涓�鍚� .writing 涓存椂鏂囦欢锛孋REATE_NEW+鍗曟 write 璋冪敤鍐呭啓瀹� flush+close
+ Files.write(writing, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
+ try {
+ // 绗簩姝ワ細鍘熷瓙鏀瑰悕鍙戝竷涓� .retry鈥斺�攔ename 鍘熷瓙鎬т娇鍙壂鎻忓悕涓嬪彧鍙兘鍑虹幇瀹屾暣鍐呭
+ Files.move(writing, published, StandardCopyOption.ATOMIC_MOVE);
+ return; // 鍙戝竷鎴愬姛
+ } catch (NoSuchFileException sourceGone) {
+ // 婧愬凡琚� AuditSpoolReplayer#recoverOrphanedWriting() 闅旂涓� .stale锛堟湰娆″啓鍏ユ浘鍦�
+ // Files.write 鍐呰闃诲瓒呰繃瀛ゅ効鍥炴敹闃堝�硷級鈥斺�斿唴瀹规湭涓紝鍙槸婊炵暀闅旂鍖恒�傚彧閲嶈瘯涓�娆★細
+ // 鎹㈡柊 UUID 閲嶅啓涓�浠藉啀鍙戝竷锛�.stale 閲岀殑鏃у壇鏈嚦澶氶�犳垚浜哄伐琛ュ綍鏃剁殑閲嶅锛堝箓绛夊惛鏀讹級
+ if (retried) {
+ log.error("AUDIT-SPOOL-RETRY-PUBLISH-FAIL file={} clientEventId={} "
+ + "婧愭枃浠跺凡琚殧绂讳笖閲嶈瘯鍚庝粛澶辫触锛岄渶浜哄伐鏍告煡 .stale 闅旂鍖鸿ˉ褰�",
+ writing.getFileName(),
+ event == null ? null : event.getClientEventId(), sourceGone);
+ return;
+ }
+ retried = true;
+ log.warn("[audit] degraded 鍙戝竷婧愭枃浠惰闅旂锛堝啓鍏ユ浘闃诲瓒呰繃瀛ゅ効鍥炴敹闃堝�硷級锛岃嚜鎰堥噸鍐欎竴娆�: "
+ + "file={} clientEventId={}",
+ writing.getFileName(), event == null ? null : event.getClientEventId());
+ // 缁х画寰幆锛氭柊 UUID 閲嶅啓涓�浠藉啀灏濊瘯鍙戝竷
+ } catch (IOException renameFail) {
+ // 鍏跺畠鏋佺 IO 寮傚父锛堢鐩樻弧/鏉冮檺绐佸彉绛夛級锛氬唴瀹瑰凡瀹屾暣钀藉湪 .writing锛堜笂涓�姝ュ啓鍏ュ凡鎴愬姛锛夛紝
+ // 涓嶇畻浜嬩欢涓㈠け鈥斺�斾笉璁″叆 discardCount锛屼繚鐣� .writing 浜ゅ鍎垮洖鏀跺厹搴�
+ log.error("AUDIT-SPOOL-RETRY-PUBLISH-FAIL file={} clientEventId={} "
+ + "鍘熷瓙鍙戝竷澶辫触锛屽唴瀹瑰凡淇濈暀浜� .writing 寰呭鍎垮洖鏀�",
+ writing.getFileName(),
+ event == null ? null : event.getClientEventId(), renameFail);
+ return;
+ }
+ }
+ }
+
+ /** 绱鍥犺惤鐩� IO 澶辫触鑰屼涪寮冪殑浜嬩欢鏁帮紙瑙傛祴鐢級銆� */
+ public long discardCount() {
+ return discardCount.get();
+ }
+}
--
Gitblit v1.8.0