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