feat: batch deleteusers for pending machine-user deletions
Configurable batch size; mark DB deleted only after successful device delete.main
parent
5e80e11b0e
commit
0773ee3311
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace HanvonF710XAttendanceService
|
||||||
|
{
|
||||||
|
internal sealed class MachineUserDeleteItem
|
||||||
|
{
|
||||||
|
public MachineUserDeleteItem(string serialNumber, string deviceEnrollId)
|
||||||
|
{
|
||||||
|
SerialNumber = serialNumber ?? "";
|
||||||
|
DeviceEnrollId = deviceEnrollId ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SerialNumber { get; }
|
||||||
|
public string DeviceEnrollId { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MachineUserDeleteBatcher
|
||||||
|
{
|
||||||
|
public static int CountBatches(int itemCount, int batchSize)
|
||||||
|
{
|
||||||
|
if (itemCount <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = batchSize > 0 ? batchSize : MachineUserDeleteSettings.DefaultBatchSize;
|
||||||
|
return (itemCount + size - 1) / size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<List<T>> SplitIntoBatches<T>(IReadOnlyList<T> items, int batchSize)
|
||||||
|
{
|
||||||
|
var batches = new List<List<T>>();
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
{
|
||||||
|
return batches;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size = batchSize > 0 ? batchSize : MachineUserDeleteSettings.DefaultBatchSize;
|
||||||
|
for (int i = 0; i < items.Count; i += size)
|
||||||
|
{
|
||||||
|
int take = Math.Min(size, items.Count - i);
|
||||||
|
var batch = new List<T>(take);
|
||||||
|
for (int j = 0; j < take; j++)
|
||||||
|
{
|
||||||
|
batch.Add(items[i + j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
batches.Add(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using System.Configuration;
|
||||||
|
|
||||||
|
namespace HanvonF710XAttendanceService
|
||||||
|
{
|
||||||
|
internal static class MachineUserDeleteSettings
|
||||||
|
{
|
||||||
|
public const int DefaultBatchSize = 20;
|
||||||
|
public const int DefaultHttpTimeoutMs = 60000;
|
||||||
|
|
||||||
|
public static int GetBatchSize()
|
||||||
|
{
|
||||||
|
return GetPositiveInt("DEVICE_DELETE_BATCH_SIZE", DefaultBatchSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetHttpTimeoutMs()
|
||||||
|
{
|
||||||
|
return GetPositiveInt("DEVICE_HTTP_TIMEOUT_MS", DefaultHttpTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetPositiveInt(string key, int defaultValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = ConfigurationManager.AppSettings[key];
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int.TryParse(raw.Trim(), out int value) && value > 0 ? value : defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace HanvonF710XAttendanceService
|
||||||
|
{
|
||||||
|
internal delegate bool MachineUserDeleteBatchDelegate(IReadOnlyList<string> deviceEnrollIds, out string error);
|
||||||
|
|
||||||
|
internal delegate void MachineUserDeletedDelegate(string serialNumber);
|
||||||
|
|
||||||
|
internal sealed class MachineUserDeleteSyncResult
|
||||||
|
{
|
||||||
|
public int TotalPending { get; set; }
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
public int FailedCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class MachineUserDeleteSync
|
||||||
|
{
|
||||||
|
public static MachineUserDeleteSyncResult Execute(
|
||||||
|
string machineId,
|
||||||
|
string targetIp,
|
||||||
|
IReadOnlyList<MachineUserDeleteItem> pendingItems,
|
||||||
|
int batchSize,
|
||||||
|
MachineUserDeleteBatchDelegate deleteBatch,
|
||||||
|
MachineUserDeletedDelegate markDeleted,
|
||||||
|
Action<string> log)
|
||||||
|
{
|
||||||
|
var result = new MachineUserDeleteSyncResult();
|
||||||
|
if (pendingItems == null || pendingItems.Count == 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.TotalPending = pendingItems.Count;
|
||||||
|
var batches = MachineUserDeleteBatcher.SplitIntoBatches(pendingItems, batchSize);
|
||||||
|
int totalBatches = batches.Count;
|
||||||
|
|
||||||
|
log?.Invoke($"[USER_DELETE] machine={machineId} target={targetIp} pending={result.TotalPending} batchSize={batchSize} batches={totalBatches}");
|
||||||
|
|
||||||
|
for (int batchIndex = 0; batchIndex < batches.Count; batchIndex++)
|
||||||
|
{
|
||||||
|
var batch = batches[batchIndex];
|
||||||
|
int batchNumber = batchIndex + 1;
|
||||||
|
var deviceIds = new List<string>(batch.Count);
|
||||||
|
foreach (var item in batch)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(item.DeviceEnrollId))
|
||||||
|
{
|
||||||
|
deviceIds.Add(item.DeviceEnrollId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} count={batch.Count}");
|
||||||
|
|
||||||
|
if (deviceIds.Count == 0)
|
||||||
|
{
|
||||||
|
log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} result=FAILED err=no_device_ids");
|
||||||
|
result.FailedCount += batch.Count;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string error = null;
|
||||||
|
if (deleteBatch == null || !deleteBatch(deviceIds, out error))
|
||||||
|
{
|
||||||
|
log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} result=FAILED err={error ?? "deleteusers failed"}");
|
||||||
|
result.FailedCount += batch.Count;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in batch)
|
||||||
|
{
|
||||||
|
markDeleted?.Invoke(item.SerialNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.SuccessCount += batch.Count;
|
||||||
|
log?.Invoke($"[USER_DELETE] batch={batchNumber}/{totalBatches} result=SUCCESS");
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.Invoke($"[USER_DELETE] machine={machineId} total={result.TotalPending} success={result.SuccessCount} failed={result.FailedCount}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace HanvonF710XAttendanceService
|
||||||
|
{
|
||||||
|
internal static class MachineUserDeleteSyncTests
|
||||||
|
{
|
||||||
|
public static int RunAll()
|
||||||
|
{
|
||||||
|
int failed = 0;
|
||||||
|
failed += Run("651 IDs split into 33 batches of 20", Test651IdsSplitInto33Batches);
|
||||||
|
failed += Run("exactly 20 IDs is one batch", TestExactly20IdsOneBatch);
|
||||||
|
failed += Run("fewer than 20 IDs is one batch", TestFewerThan20IdsOneBatch);
|
||||||
|
failed += Run("empty deletion list", TestEmptyDeletionList);
|
||||||
|
failed += Run("successful batch marks DB deleted", TestSuccessfulBatchMarksDeleted);
|
||||||
|
failed += Run("failed batch remains pending", TestFailedBatchRemainsPending);
|
||||||
|
return failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Run(string name, Func<bool> test)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!test())
|
||||||
|
{
|
||||||
|
Console.WriteLine("FAIL: " + name);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("PASS: " + name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("FAIL: " + name + " err=" + ex.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Test651IdsSplitInto33Batches()
|
||||||
|
{
|
||||||
|
var ids = BuildIds(651);
|
||||||
|
var batches = MachineUserDeleteBatcher.SplitIntoBatches(ids, 20);
|
||||||
|
return batches.Count == 33
|
||||||
|
&& batches.Take(32).All(b => b.Count == 20)
|
||||||
|
&& batches[32].Count == 11
|
||||||
|
&& MachineUserDeleteBatcher.CountBatches(651, 20) == 33;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestExactly20IdsOneBatch()
|
||||||
|
{
|
||||||
|
var ids = BuildIds(20);
|
||||||
|
var batches = MachineUserDeleteBatcher.SplitIntoBatches(ids, 20);
|
||||||
|
return batches.Count == 1 && batches[0].Count == 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestFewerThan20IdsOneBatch()
|
||||||
|
{
|
||||||
|
var ids = BuildIds(7);
|
||||||
|
var batches = MachineUserDeleteBatcher.SplitIntoBatches(ids, 20);
|
||||||
|
return batches.Count == 1 && batches[0].Count == 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestEmptyDeletionList()
|
||||||
|
{
|
||||||
|
var batches = MachineUserDeleteBatcher.SplitIntoBatches(new List<string>(), 20);
|
||||||
|
var result = MachineUserDeleteSync.Execute(
|
||||||
|
"105",
|
||||||
|
"192.168.90.223",
|
||||||
|
new List<MachineUserDeleteItem>(),
|
||||||
|
20,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
return batches.Count == 0
|
||||||
|
&& result.TotalPending == 0
|
||||||
|
&& result.SuccessCount == 0
|
||||||
|
&& result.FailedCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestSuccessfulBatchMarksDeleted()
|
||||||
|
{
|
||||||
|
var pending = new List<MachineUserDeleteItem>
|
||||||
|
{
|
||||||
|
new MachineUserDeleteItem("1001", "1001"),
|
||||||
|
new MachineUserDeleteItem("1002", "1002"),
|
||||||
|
new MachineUserDeleteItem("1003", "1003")
|
||||||
|
};
|
||||||
|
var marked = new List<string>();
|
||||||
|
var logs = new List<string>();
|
||||||
|
|
||||||
|
var result = MachineUserDeleteSync.Execute(
|
||||||
|
"105",
|
||||||
|
"192.168.90.223",
|
||||||
|
pending,
|
||||||
|
20,
|
||||||
|
(IReadOnlyList<string> ids, out string error) =>
|
||||||
|
{
|
||||||
|
error = null;
|
||||||
|
return ids.Count == 3;
|
||||||
|
},
|
||||||
|
serial => marked.Add(serial),
|
||||||
|
logs.Add);
|
||||||
|
|
||||||
|
return result.TotalPending == 3
|
||||||
|
&& result.SuccessCount == 3
|
||||||
|
&& result.FailedCount == 0
|
||||||
|
&& marked.Count == 3
|
||||||
|
&& marked.Contains("1001")
|
||||||
|
&& marked.Contains("1002")
|
||||||
|
&& marked.Contains("1003")
|
||||||
|
&& logs.Any(l => l.Contains("result=SUCCESS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestFailedBatchRemainsPending()
|
||||||
|
{
|
||||||
|
var pending = BuildDeleteItems(40);
|
||||||
|
var marked = new List<string>();
|
||||||
|
int callCount = 0;
|
||||||
|
|
||||||
|
var result = MachineUserDeleteSync.Execute(
|
||||||
|
"105",
|
||||||
|
"192.168.90.223",
|
||||||
|
pending,
|
||||||
|
20,
|
||||||
|
(IReadOnlyList<string> ids, out string error) =>
|
||||||
|
{
|
||||||
|
callCount++;
|
||||||
|
if (callCount == 2)
|
||||||
|
{
|
||||||
|
error = "The operation has timed out";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
serial => marked.Add(serial),
|
||||||
|
_ => { });
|
||||||
|
|
||||||
|
return result.TotalPending == 40
|
||||||
|
&& result.SuccessCount == 20
|
||||||
|
&& result.FailedCount == 20
|
||||||
|
&& marked.Count == 20
|
||||||
|
&& !marked.Contains("21")
|
||||||
|
&& marked.Contains("1")
|
||||||
|
&& marked.Contains("20");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> BuildIds(int count)
|
||||||
|
{
|
||||||
|
var ids = new List<string>(count);
|
||||||
|
for (int i = 1; i <= count; i++)
|
||||||
|
{
|
||||||
|
ids.Add(i.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MachineUserDeleteItem> BuildDeleteItems(int count)
|
||||||
|
{
|
||||||
|
var items = new List<MachineUserDeleteItem>(count);
|
||||||
|
for (int i = 1; i <= count; i++)
|
||||||
|
{
|
||||||
|
items.Add(new MachineUserDeleteItem(i.ToString(), i.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue