-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathWatchDbPoller.cs
More file actions
106 lines (101 loc) · 4 KB
/
Copy pathWatchDbPoller.cs
File metadata and controls
106 lines (101 loc) · 4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
using System.Text;
using Microsoft.Extensions.Logging;
using Npgsql;
using NpgsqlRest;
namespace NpgsqlRestClient;
/// <summary>
/// Database change detection for watch mode. There is no filesystem to watch for routine-source
/// endpoints — instead, the poller runs the SAME discovery query the routine source uses (same
/// configured filters — see <see cref="RoutineSource.CreateFingerprintCommand"/>), hashed server-side
/// into one scalar, on a dedicated non-pooled connection. If the hash changes, the discovered
/// endpoints changed — by definition: create/replace/drop/alter of functions and procedures, grants,
/// COMMENT ON (annotations), schema renames, and changes to the composite/table types their signatures
/// use. Anything the query does not read (an unrelated table, temp objects, data) can never trigger.
/// </summary>
public sealed class WatchDbPoller(
string connectionString,
TimeSpan interval,
Action onChange,
ILogger? logger,
IReadOnlyList<RoutineSource> sources)
{
private readonly string _connString = new NpgsqlConnectionStringBuilder(connectionString) { Pooling = false }.ConnectionString;
private NpgsqlConnection? _conn;
private string? _baseline;
private volatile bool _rebaseline;
/// <summary>Fingerprint of one source's discovery result on an open connection (also used by tests).</summary>
public static string? GetFingerprint(NpgsqlConnection connection, RoutineSource source)
{
using var cmd = source.CreateFingerprintCommand(connection);
return cmd.ExecuteScalar() as string;
}
/// <summary>
/// Forget the baseline so the next tick captures a fresh one WITHOUT firing. Called after work
/// that legitimately changes the database (a test rerun with committed fixtures, a restart) so
/// self-inflicted changes never trigger.
/// </summary>
public void Rebaseline() => _rebaseline = true;
/// <summary>Poll loop; runs until cancelled. Connection failures skip the tick and retry.</summary>
public async Task RunAsync(CancellationToken ct)
{
if (sources.Count == 0)
{
return;
}
while (ct.IsCancellationRequested is false)
{
try
{
await Task.Delay(interval, ct);
}
catch (OperationCanceledException)
{
break;
}
try
{
if (_conn is null || _conn.State != System.Data.ConnectionState.Open)
{
_conn?.Dispose();
_conn = new NpgsqlConnection(_connString);
await _conn.OpenAsync(ct);
}
var sb = new StringBuilder();
foreach (var source in sources)
{
await using var cmd = source.CreateFingerprintCommand(_conn);
sb.Append(await cmd.ExecuteScalarAsync(ct) as string).Append('|');
}
var current = sb.ToString();
if (_rebaseline)
{
_rebaseline = false;
_baseline = current;
continue;
}
if (_baseline is null)
{
_baseline = current;
continue;
}
if (string.Equals(_baseline, current, StringComparison.Ordinal) is false)
{
_baseline = current;
onChange();
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
// DB down / restarting: skip this tick, reconnect on the next one.
logger?.LogDebug("watch: database poll failed ({Message}) — retrying", ex.Message);
try { _conn?.Dispose(); } catch { }
_conn = null;
}
}
try { _conn?.Dispose(); } catch { }
}
}