Perf(license): mutex + write-suppression for licenses.ini hot path
licenses.ini was hit on every heartbeat -- 5s x clients x ~8 SetStr per
auth -- with no concurrency protection. Two consequences:
1. 100 concurrent online would saturate the file (~160 writes/sec,
full-file rewrite each via WritePrivateProfileString).
2. Concurrent SetPendingRenewal / DecrementPendingQuota with no lock
occasionally clobbered freshly-set renewal quotas (reported by
user as "preset renewal silently disappears").
Add LicensesIniMutex() (Meyers singleton recursive_mutex, exposed in
CPasswordDlg.h so both CPasswordDlg.cpp and CLicenseDlg.cpp share it)
and wrap all 15 functions that touch licenses.ini.
Rewrite UpdateLicenseActivity around g_activityCache (in-memory state
keyed by "SN|IP|machine"): skip the entire write path when nothing
changed and the 30s LastActiveTime throttle window hasn't expired.
Passcode/HMAC are only flushed on actual change (renewal path); IP
list is only rewritten when the yyMMdd timestamp would roll a day.
Measured impact (local 2-client baseline):
before: 0.60 writes/sec (4 writes per heartbeat cluster)
after: 0.07 writes/sec (one write per client per 30s throttle)
Extrapolated to the 100-online target:
before: ~160 writes/sec (saturation)
after: ~3.3 writes/sec (100 clients / 30s throttle window)
Race elimination is the more important win: PendingQuota's
read-modify-write is now atomic, so the "preset renewal disappears"
race is closed.
Notes from audit (these landed during the same iteration):
- Cache key is (SN, IP, machine), not SN alone. A single SN can be
shared by 100+ end machines in bulk-license deployments, so a
per-SN cache flips on every heartbeat and defeats suppression.
Per-(SN, IP, machine) throttling is what makes the 100/30 model
actually hold; an SN-only key reproduced the original ~0.7 writes/s.
- DeleteLicense invalidates the per-SN activity cache via
InvalidateLicenseActivityCache() (prefix scan since one SN maps to
many cache entries). Without this, cache hits after delete would
skip the auto-recreate path and leave the section permanently
missing.
- OnLicenseViewIPs: m_ListLicense.SetItemText moved outside the lock
so the critical section only covers disk I/O.
This commit was merged in pull request #2.
This commit is contained in:
@@ -55,9 +55,13 @@ static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
|
||||
// 获取所有授权信息
|
||||
std::vector<LicenseInfo> GetAllLicenses()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::vector<LicenseInfo> licenses;
|
||||
std::string iniPath = GetLicensesPath();
|
||||
|
||||
// 注意:CIniParser 走 ifstream 读取整文件,与 WritePrivateProfileString 的内核锁
|
||||
// 不在同一域。必须靠这里的 g_licensesIniMutex 阻止与其它写入交错,否则可能读到
|
||||
// 写入到一半的中间态。
|
||||
CIniParser parser;
|
||||
if (!parser.LoadFile(iniPath.c_str()))
|
||||
return licenses;
|
||||
@@ -306,6 +310,7 @@ void CLicenseDlg::OnSize(UINT nType, int cx, int cy)
|
||||
// 更新授权状态
|
||||
bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -457,6 +462,7 @@ int ParseHostNumFromPasscode(const std::string& passcode)
|
||||
// 设置待续期信息
|
||||
bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDate, int hostNum, int quota)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -475,6 +481,7 @@ bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDat
|
||||
// 获取待续期信息
|
||||
RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
RenewalInfo info;
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
@@ -488,6 +495,7 @@ RenewalInfo GetPendingRenewal(const std::string& deviceID)
|
||||
// 清除待续期信息
|
||||
bool ClearPendingRenewal(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -498,8 +506,11 @@ bool ClearPendingRenewal(const std::string& deviceID)
|
||||
}
|
||||
|
||||
// 配额递减,返回是否还有剩余配额
|
||||
// 关键:read-modify-write 的 PendingQuota 必须在锁内完成,否则与 SetPendingRenewal
|
||||
// 并发会丢失用户刚设置的预设续期(旧 bug:用户报告"预设续期消失"的根因)。
|
||||
bool DecrementPendingQuota(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -512,7 +523,7 @@ bool DecrementPendingQuota(const std::string& deviceID)
|
||||
cfg.SetInt(deviceID, "PendingQuota", quota);
|
||||
|
||||
if (quota <= 0) {
|
||||
// 配额用完,清除待续期信息
|
||||
// 配额用完,清除待续期信息(嵌套加锁,recursive_mutex 安全)
|
||||
ClearPendingRenewal(deviceID);
|
||||
return false;
|
||||
}
|
||||
@@ -616,6 +627,7 @@ void CLicenseDlg::OnLicenseRenewal()
|
||||
// 设置授权备注
|
||||
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -659,6 +671,7 @@ void CLicenseDlg::OnLicenseEditRemark()
|
||||
// 删除授权
|
||||
bool DeleteLicense(const std::string& deviceID)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
|
||||
@@ -679,6 +692,10 @@ bool DeleteLicense(const std::string& deviceID)
|
||||
// 删除该 section (通过写入 NULL 删除整个 section)
|
||||
BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str());
|
||||
::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存
|
||||
|
||||
// 关键:清掉 UpdateLicenseActivity 的内存缓存。否则若同 SN 客户端再次连上来,
|
||||
// cache 命中会跳过落盘 → disk 永远不会重建被删的 section。
|
||||
InvalidateLicenseActivityCache(deviceID);
|
||||
return ret != FALSE;
|
||||
}
|
||||
|
||||
@@ -860,12 +877,17 @@ void CLicenseDlg::OnLicenseViewIPs()
|
||||
|
||||
// 如果有记录被删除,保存更新后的 IP 列表
|
||||
if (removedCount > 0) {
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
|
||||
lic.IP = newIPList; // 更新内存中的数据
|
||||
// 锁内只做 I/O —— UI 控件更新(SetItemText)放锁外,避免锁内触发
|
||||
// 任何可能的消息循环回调,保持锁占用时间最短
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
|
||||
}
|
||||
lic.IP = newIPList; // 更新内存中的数据(与 m_Licenses 同步,不需要锁)
|
||||
|
||||
// 更新列表显示
|
||||
// 更新列表显示(UI 线程操作,必须在锁外)
|
||||
CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
|
||||
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay);
|
||||
}
|
||||
@@ -985,6 +1007,9 @@ bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machine
|
||||
{
|
||||
if (ip.empty()) return false;
|
||||
|
||||
// 加锁保护整个 list 遍历,避免与并发的 SetStr(IP, ...) 交错读到中间态。
|
||||
// GetAllLicenses 内部也加锁,recursive_mutex 允许嵌套。
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
auto licenses = GetAllLicenses();
|
||||
for (const auto& lic : licenses) {
|
||||
if (lic.IP.empty()) continue;
|
||||
@@ -1167,6 +1192,7 @@ void CLicenseDlg::OnLicenseAutoFrp()
|
||||
FreeFrpPortAllocation(existingPort, lic.SerialNumber); // 仅当旧端口确实归属本 SN 时才释放
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "FrpConfig", frpConfig);
|
||||
@@ -1215,6 +1241,7 @@ void CLicenseDlg::OnLicenseRevokeFrp()
|
||||
|
||||
// 清除 licenses.ini 中该授权的 FrpConfig 字段
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
|
||||
std::string iniPath = GetLicensesPath();
|
||||
config cfg(iniPath);
|
||||
cfg.SetStr(lic.SerialNumber, "FrpConfig", "");
|
||||
|
||||
Reference in New Issue
Block a user