feat(privacy): reconnect database maintenance and the manual retention run
Two sections that had backends and no buttons. Database: path, size, WAL size, message count, and a clear button. The numbers refresh at most every five seconds and not at all while a long operation owns the store -- MessageCount takes the read lock, and asking for it during a VACUUM means waiting for the whole file to be rewritten, on the draw thread. The old version called ClearMessages straight from the draw thread, VACUUM included; it runs on a worker now. One line beyond the old layout sits above the clear button: how many messages are stored, and that exporting keeps a copy. Whoever is about to throw the history away should be told there is a way not to. The legacy Chat 2 files only get a block when they are actually on disk, and the advanced tools only appear when the section is expanded with Shift held. The message injector is not back: it was deleted with the tab and writing 10,000 fake messages into a user's real database is not something to rebuild on the way past. Retention: an "apply now" button, the running hint, and the last-run line, which v1.11.0 shipped as an English literal while both strings sat translated in all 25 languages. Plus reset-to-spec next to the existing clear-overrides, since the two answer different questions and both were already translated. Retention_Apply_Tooltip stays unused and gets a replacement. It ends with "Save your changes first", and the window it was written for had a Save button. Also here, found while wiring the manual trigger: DbOperationGate.End now takes the operation it releases. It used to reset blindly, on the reasoning that a worker must be able to release from a finally without knowing whether it acquired. That is backwards: a worker whose TryBegin was refused also runs its finally, and a blind reset there hands away the lock of whichever operation actually holds it. Worse than no gate, because the refused worker walks off believing it did nothing while a VACUUM starts under somebody's open reader.
This commit is contained in:
+91
-35
@@ -1010,6 +1010,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
if (DateTimeOffset.UtcNow - Config.RetentionLastRunAt < TimeSpan.FromHours(24))
|
||||
return;
|
||||
|
||||
StartRetentionSweep(notify: false);
|
||||
}
|
||||
|
||||
// Shared by the daily check above and the manual button in settings.
|
||||
//
|
||||
// notify: the unattended sweep stays quiet, because a notification for
|
||||
// something the user did not ask for at a moment they did not choose is
|
||||
// noise. A run they pressed a button for reports back.
|
||||
//
|
||||
// Returns false when the store is already busy, so the caller can say so
|
||||
// instead of leaving the user waiting for a run that never started.
|
||||
internal bool StartRetentionSweep(bool notify)
|
||||
{
|
||||
if (DbOperations.IsBusy)
|
||||
return false;
|
||||
|
||||
// Snapshot the policy so the user can edit settings while the sweep runs.
|
||||
var policy = new Dictionary<int, int>();
|
||||
foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays)
|
||||
@@ -1023,67 +1039,107 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
var defaultDays = Config.RetentionDefaultDays;
|
||||
|
||||
_retentionSweepRunning = true;
|
||||
|
||||
// IsBackground = true so a stuck sweep never blocks plugin unload.
|
||||
new Thread(() =>
|
||||
var worker = new Thread(() =>
|
||||
{
|
||||
// Bails when anything else already owns the store, not only another
|
||||
// sweep: a user-triggered export or cleanup counts too.
|
||||
if (!DbOperations.TryBegin(Util.DbOperation.RetentionSweep))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
|
||||
Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
SaveConfig();
|
||||
if (!DbOperations.TryBegin(Util.DbOperation.RetentionSweep))
|
||||
return;
|
||||
|
||||
if (deleted > 0)
|
||||
try
|
||||
{
|
||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||
// Schedule on the next framework tick to avoid the ~194ms
|
||||
// hitch from blocking with .Wait() while the frame finishes.
|
||||
// The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is
|
||||
// now guarded by the shared Plugin.TabsListLock (B3), so this
|
||||
// tick scheduling is purely hitch-avoidance, not safety.
|
||||
// Pattern reference: SimpleTweaks
|
||||
// Tweaks/Chat/CaseInsensitiveCommands.cs:45.
|
||||
Framework.RunOnTick(() =>
|
||||
var deleted = MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
|
||||
Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
SaveConfig();
|
||||
|
||||
if (notify)
|
||||
Util.WrapperUtil.AddNotification(
|
||||
string.Format(Resources.HellionStrings.Retention_Success, deleted),
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType.Success
|
||||
);
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
// The retention thread is IsBackground=true so plugin
|
||||
// unload can fire while a scheduled tick is still
|
||||
// pending; bail before touching anything torn down.
|
||||
if (_isDisposing)
|
||||
return;
|
||||
try
|
||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||
// Schedule on the next framework tick to avoid the ~194ms
|
||||
// hitch from blocking with .Wait() while the frame finishes.
|
||||
// The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is
|
||||
// now guarded by the shared Plugin.TabsListLock (B3), so this
|
||||
// tick scheduling is purely hitch-avoidance, not safety.
|
||||
// Pattern reference: SimpleTweaks
|
||||
// Tweaks/Chat/CaseInsensitiveCommands.cs:45.
|
||||
Framework.RunOnTick(() =>
|
||||
{
|
||||
MessageManager.ClearAllTabs();
|
||||
MessageManager.FilterAllTabs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Retention sweep clear+refilter failed");
|
||||
}
|
||||
});
|
||||
// The retention thread is IsBackground=true so plugin
|
||||
// unload can fire while a scheduled tick is still
|
||||
// pending; bail before touching anything torn down.
|
||||
if (_isDisposing)
|
||||
return;
|
||||
try
|
||||
{
|
||||
MessageManager.ClearAllTabs();
|
||||
MessageManager.FilterAllTabs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Retention sweep clear+refilter failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information("Retention sweep ran, nothing expired.");
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
Log.Information("Retention sweep ran, nothing expired.");
|
||||
DbOperations.End(Util.DbOperation.RetentionSweep);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Retention sweep failed");
|
||||
if (notify)
|
||||
Util.WrapperUtil.AddNotification(
|
||||
Resources.HellionStrings.Retention_Error,
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType.Error
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DbOperations.End();
|
||||
_retentionSweepRunning = false;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
}.Start();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
worker.Start();
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// The thread never ran, so nothing will clear the flag for us.
|
||||
_retentionSweepRunning = false;
|
||||
Log.Error(e, "Could not start the retention sweep thread");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Read by the settings tab every frame so the manual button can say a run is
|
||||
// in progress. The gate itself cannot answer that: it goes busy only once
|
||||
// the worker reaches TryBegin, which is after Start returns.
|
||||
private volatile bool _retentionSweepRunning;
|
||||
|
||||
internal bool RetentionSweepRunning => _retentionSweepRunning;
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
// v1.9.0 B5: time the whole handler (style + font prologue included).
|
||||
|
||||
Reference in New Issue
Block a user