feat: batch deleteusers for pending machine-user deletions

Configurable batch size; mark DB deleted only after successful device delete.
main
SYED MUSTUFA AHMED NAQVI 2026-09-04 12:37:19 +05:00
parent 5e80e11b0e
commit 0773ee3311
4 changed files with 350 additions and 0 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

83
MachineUserDeleteSync.cs Normal file
View File

@ -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;
}
}
}

View File

@ -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;
}
}
}