sycommon-python-lib 0.2.7a25__py3-none-any.whl → 0.2.7a26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -211,6 +211,17 @@ class WeComLDAPService(metaclass=SingletonMeta):
211
211
  # 企微部门 ID -> 名称 缓存
212
212
  self._dept_map: Dict[int, str] = {}
213
213
 
214
+ # 🔑 resolve 进程内 L1 缓存 + per-userid 并发去重锁
215
+ # 背景:企微 push 把「文件+文字」拆成两条并发 asyncio task,各自在 handler
216
+ # 入口 await resolve_unified_user_id。Redis(L2) 缓存写入有传播/竞态延迟,
217
+ # 并发的两个请求互相看不到对方刚写的 Redis → 各自查一遍企微 user/get API
218
+ # (~1-2s/次)→ 文件 handler 入口被卡 2s+ → 注册 _user_tasks 晚 → 文字抢先
219
+ # 起 agent → 双气泡。L1 命中直接返回;per-userid 锁保证同一人同时只查一次,
220
+ # 其余并发请求等结果复用。TTL 短(5min),Redis 仍负责跨进程持久化。
221
+ self._resolve_cache: Dict[str, tuple] = {} # wecom_userid -> (unified_id, expire_ts)
222
+ self._resolve_locks: Dict[str, asyncio.Lock] = {}
223
+ self._resolve_ttl: int = 300 # 进程内 L1 缓存 5 分钟
224
+
214
225
  # 订阅配置热更新(幂等)
215
226
  from sycommon.config.config_change_notifier import notifier
216
227
  notifier.register("wecom_ldap", sync_cb=self.reload)
@@ -232,6 +243,8 @@ class WeComLDAPService(metaclass=SingletonMeta):
232
243
  # access_token 直接作废,避免用旧 secret 换来的 token 继续生效。
233
244
  self._access_token = None
234
245
  self._token_expires_at = 0
246
+ # 配置热更新时清空 resolve L1 缓存,避免用旧映射继续返回。
247
+ self._resolve_cache.clear()
235
248
 
236
249
  # ─── 企微 API ───
237
250
 
@@ -737,8 +750,10 @@ class WeComLDAPService(metaclass=SingletonMeta):
737
750
  async def resolve_unified_user_id(self, wecom_userid: str) -> str:
738
751
  """将企微 userid 解析为统一 user_id(LDAP sAMAccountName)
739
752
 
740
- 优先从 Redis 缓存读取,缓存未命中时查 LDAP 并写入 Redis。
741
- 匹配到 LDAP 返回 sAMAccountName,否则直接使用企微 userid
753
+ 优先从进程内 L1 缓存读取(命中直接返回,无任何网络开销),其次 Redis(L2) 缓存,
754
+ 最后查企微+LDAP 并写入两层缓存。per-userid 锁保证同一 userid 的并发请求只查一次
755
+ API,其余等结果复用——这是修复「文件+文字并发各查一遍企微 API → 文件 handler
756
+ 入口被卡 → 双气泡」的关键。
742
757
 
743
758
  Args:
744
759
  wecom_userid: 企微用户ID(如 "0633"、密文 open_userid)
@@ -746,33 +761,55 @@ class WeComLDAPService(metaclass=SingletonMeta):
746
761
  Returns:
747
762
  统一 user_id(小写,如 "osulcode.xiao")
748
763
  """
749
- # 1. 查 Redis 缓存
750
- cached = await self._get_cached_mapping(wecom_userid)
751
- if cached:
752
- return cached
753
-
754
- # 2. 查企微 + LDAP
755
- unified_id = wecom_userid
756
- employee_id = wecom_userid # 默认用工号就是原始 userid
757
- try:
758
- result = await self.get_user_info(wecom_userid)
759
- employee_id = result.wecom_userid # get_user_info 返回解码后的明文工号
760
- if result.matched and result.ldap_user and result.ldap_user.username:
761
- unified_id = result.ldap_user.username
762
- SYLogger.info(
763
- f"[WeComLDAP] 用户映射: wecom={wecom_userid} -> ldap={unified_id}")
764
- else:
765
- SYLogger.info(
766
- f"[WeComLDAP] 用户映射: wecom={wecom_userid} -> 未匹配LDAP,回退={unified_id}")
767
- except Exception as e:
768
- SYLogger.warning(
769
- f"[WeComLDAP] 解析统一 user_id 失败: wecom_userid={wecom_userid}, error={e}")
764
+ now = time.time()
765
+
766
+ # 0. L1 进程内缓存(最快,无网络)
767
+ hit = self._resolve_cache.get(wecom_userid)
768
+ if hit and hit[1] > now:
769
+ return hit[0]
770
+
771
+ # per-userid 锁:同一 userid 并发只放一个进去查,其余等结果
772
+ lock = self._resolve_locks.get(wecom_userid)
773
+ if lock is None:
774
+ lock = asyncio.Lock()
775
+ self._resolve_locks[wecom_userid] = lock
776
+ async with lock:
777
+ # 双检:拿到锁后可能已被前一个并发请求填好 L1
778
+ now = time.time()
779
+ hit = self._resolve_cache.get(wecom_userid)
780
+ if hit and hit[1] > now:
781
+ return hit[0]
782
+
783
+ # 1. 查 Redis 缓存(L2)
784
+ cached = await self._get_cached_mapping(wecom_userid)
785
+ if cached:
786
+ self._resolve_cache[wecom_userid] = (cached, now + self._resolve_ttl)
787
+ return cached
788
+
789
+ # 2. 查企微 + LDAP
790
+ unified_id = wecom_userid
791
+ employee_id = wecom_userid # 默认用工号就是原始 userid
792
+ try:
793
+ result = await self.get_user_info(wecom_userid)
794
+ employee_id = result.wecom_userid # get_user_info 返回解码后的明文工号
795
+ if result.matched and result.ldap_user and result.ldap_user.username:
796
+ unified_id = result.ldap_user.username
797
+ SYLogger.info(
798
+ f"[WeComLDAP] 用户映射: wecom={wecom_userid} -> ldap={unified_id}")
799
+ else:
800
+ SYLogger.info(
801
+ f"[WeComLDAP] 用户映射: wecom={wecom_userid} -> 未匹配LDAP,回退={unified_id}")
802
+ except Exception as e:
803
+ SYLogger.warning(
804
+ f"[WeComLDAP] 解析统一 user_id 失败: wecom_userid={wecom_userid}, error={e}")
770
805
 
771
- # 3. 写入 Redis 缓存(统一小写)
772
- unified_id = unified_id.lower().replace(" ", "")
773
- await self._set_cached_mapping(employee_id, unified_id)
806
+ # 3. 写入 Redis 缓存(统一小写)
807
+ unified_id = unified_id.lower().replace(" ", "")
808
+ await self._set_cached_mapping(employee_id, unified_id)
774
809
 
775
- return unified_id
810
+ # 写入 L1
811
+ self._resolve_cache[wecom_userid] = (unified_id, time.time() + self._resolve_ttl)
812
+ return unified_id
776
813
 
777
814
  async def get_employee_id(self, unified_id: str) -> Optional[str]:
778
815
  """从 Redis 获取企微员工工号
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.2.7a25
3
+ Version: 0.2.7a26
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -166,7 +166,7 @@ sycommon/auth/ldap_service.py,sha256=fOcpVov5LWJkBk62qbTaltks1c4la7JsbD104KfdBOI
166
166
  sycommon/auth/oa_cache.py,sha256=u673y-mK-xo24pSqvfjL68_YFASO2zoxd_iAQ0HetCo,3491
167
167
  sycommon/auth/oa_crypto.py,sha256=xpY1R1Bj3KLENXB0TuThB6Eku1E9PYjcoSpOdgDmCgc,1898
168
168
  sycommon/auth/oa_service.py,sha256=kLepV9zgqpZoaB73DRPpMA5tJJQjoaDtQPdzBcGXeak,6235
169
- sycommon/auth/wecom_ldap_service.py,sha256=5DWXu-mR3ckZm2lkCJu7gLi_3GCiLNZNpK3bFCPcQAc,31122
169
+ sycommon/auth/wecom_ldap_service.py,sha256=atVuYs8zL1EfEN5yX9bwHcHpCsB3BdnMnMlR2bbmD6c,33416
170
170
  sycommon/config/ACPAgentConfig.py,sha256=paAa0c0gy1RnDDum-Q6m7QLTe7lhz2dsFt4r72QtmWE,3202
171
171
  sycommon/config/Config.py,sha256=MP92XxB7hsQiOnyjMkZskbxuMn_OUAZ4RTbC74ZATl4,7885
172
172
  sycommon/config/DatabaseConfig.py,sha256=ILiUuYT9_xJZE2W-RYuC3JCt_YLKc1sbH13-MHIOPhg,804
@@ -308,8 +308,8 @@ sycommon/tools/syemail.py,sha256=BDFhgf7WDOQeTcjxJEQdu0dQhnHFPO_p3eI0-Ni3LhQ,561
308
308
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
309
309
  sycommon/xxljob/__init__.py,sha256=7eoBlQxv-B39IfRSCY2bkqdGYs1QRe1umAWd88VMEEM,86
310
310
  sycommon/xxljob/xxljob_service.py,sha256=1yifwIBNGsCIxLnQjHKiBlbsigc_zvPH-dMTZcNxe-Q,7649
311
- sycommon_python_lib-0.2.7a25.dist-info/METADATA,sha256=h6F3kRXGeBg5wu9iLFgCXfgRQzfdRte-f-7NhXuBuvg,7962
312
- sycommon_python_lib-0.2.7a25.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
313
- sycommon_python_lib-0.2.7a25.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
314
- sycommon_python_lib-0.2.7a25.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
315
- sycommon_python_lib-0.2.7a25.dist-info/RECORD,,
311
+ sycommon_python_lib-0.2.7a26.dist-info/METADATA,sha256=DBrGqUqt5wJG8QAqbbRk7ZfswekMXYBMztNXrW0BnhM,7962
312
+ sycommon_python_lib-0.2.7a26.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
313
+ sycommon_python_lib-0.2.7a26.dist-info/entry_points.txt,sha256=gsR4SssKxDWjRU8ggidzNcdMXDPRSKRS7UaGyNP84Qg,92
314
+ sycommon_python_lib-0.2.7a26.dist-info/top_level.txt,sha256=RgphKrg7nJyZ7irJqbxFr-5H2LUYTvI7ivoWZH2hcD0,29
315
+ sycommon_python_lib-0.2.7a26.dist-info/RECORD,,