From 89ec6476fcde8b7bc18418aa66619337e8da6b7b Mon Sep 17 00:00:00 2001 From: Bobbie Hodgetts Date: Mon, 11 May 2026 22:21:39 +0100 Subject: [PATCH] Added sql server ping feature, check for online server (#51) --- src/bnhtrade.Core/Data/Database/Connection.cs | 106 ++++++++---------- .../Data/Database/SKU/Price/ReadParameter.cs | 5 - src/bnhtrade.Core/Data/Database/ServerPing.cs | 87 ++++++++++++++ .../Data/Database/UnitOfWork/Connection.cs | 55 --------- .../Logic/Stock/SkuTransactionTypeCrud.cs | 2 +- .../Logic/Utilities/NightlyRoutine.cs | 16 +++ .../Logic/Utilities/SqlServerPing.cs | 25 +++++ .../Model/Credentials/AmazonSPAPI.cs | 28 ----- .../Model/Credentials/bnhtradeDB.cs | 43 ------- src/bnhtrade.ScheduledTasks/Program.cs | 16 ++- 10 files changed, 188 insertions(+), 195 deletions(-) create mode 100644 src/bnhtrade.Core/Data/Database/ServerPing.cs delete mode 100644 src/bnhtrade.Core/Data/Database/UnitOfWork/Connection.cs create mode 100644 src/bnhtrade.Core/Logic/Utilities/SqlServerPing.cs delete mode 100644 src/bnhtrade.Core/Model/Credentials/AmazonSPAPI.cs delete mode 100644 src/bnhtrade.Core/Model/Credentials/bnhtradeDB.cs diff --git a/src/bnhtrade.Core/Data/Database/Connection.cs b/src/bnhtrade.Core/Data/Database/Connection.cs index bfdc612..c171de3 100644 --- a/src/bnhtrade.Core/Data/Database/Connection.cs +++ b/src/bnhtrade.Core/Data/Database/Connection.cs @@ -1,92 +1,78 @@ -using System; +using FikaAmazonAPI.AmazonSpApiSDK.Models.Services; +using System; using System.Configuration; namespace bnhtrade.Core.Data.Database { + /// this class needs a sort out. Ideally it shoud be called what it is, a connection string builder, and + /// it should expose a method to serve the connection string--rather than class inheritance and using a property setter + /// something to do once there aren't so many open git branches + /// public class Connection { - //protected readonly string SqlConnectionString; - private Model.Credentials.bnhtradeDB _dbCredentials; + private string _server; + private string _user; + private string _userPassword; + private string _database = "e2A"; + private bool _persistSecurityInfo = true; + private bool _multipleActiveResultSets = true; + private bool _encrypt = true; + private uint _connectRetryInterval; + private uint _connectRetryCount; + private uint _connectTimeout; - protected string SqlConnectionString - { - get { return _dbCredentials.ConnectionString; } - } + internal string SqlConnectionString { get; private set; } - public Connection() + /// + /// + /// + /// Retry interval in seconds, must be 5-60 + /// Timeout length in seconds (0 is indefinitely) + /// + /// + public Connection(uint connectRetryInterval = 10, uint connectRetryCount = 6, uint connectionTimeout = 60) { - var config = new Config().GetConfiguration(); + if (connectRetryInterval < 5 || connectRetryInterval > 60) + { + // these are limits set by the sql server + throw new ArgumentOutOfRangeException("ConnectRetryInterval must be from 5 to 60 seconds"); + } + _connectRetryInterval = connectRetryInterval; + _connectRetryCount = connectRetryCount; + _connectTimeout = connectionTimeout; - // attempt to retrive credentials from app.local.config + // retrive credentials from app.local.config + var config = new Config().GetConfiguration(); try { - string dataSource = config.AppSettings.Settings["DbDataSource"].Value; - string userId = config.AppSettings.Settings["DbUserId"].Value; - string pass = config.AppSettings.Settings["DbUserPassword"].Value; + _server = config.AppSettings.Settings["DbDataSource"].Value; + _user = config.AppSettings.Settings["DbUserId"].Value; + _userPassword = config.AppSettings.Settings["DbUserPassword"].Value; // check - if (string.IsNullOrEmpty(dataSource)) + if (string.IsNullOrEmpty(_server)) { throw new ArgumentException("Could not retrive 'DbDataSource' from config file"); } - else if (string.IsNullOrEmpty(userId)) + else if (string.IsNullOrEmpty(_user)) { throw new ArgumentException("Could not retrive 'DbUserId' from config file"); } - else if (string.IsNullOrEmpty(pass)) + else if (string.IsNullOrEmpty(_userPassword)) { throw new ArgumentException("Could not retrive 'DbUserPassword' from config file"); } - - var dbCredentials = new bnhtrade.Core.Model.Credentials.bnhtradeDB(dataSource, userId, pass); - this._dbCredentials = dbCredentials; } catch (Exception ex) { throw new Exception("Unable to retirve DB credentials: " + ex.Message); } - } - private void ConnectionOld() - { - // attempt to retrive credentials from app.local.config - try - { - string dataSource = ConfigurationManager.AppSettings["DbDataSource"]; - string userId = ConfigurationManager.AppSettings["DbUserId"]; - string pass = ConfigurationManager.AppSettings["DbUserPassword"]; - - // check - if (string.IsNullOrEmpty(dataSource)) - { - throw new ArgumentException("Could not retrive 'DbDataSource' from config file"); - } - else if (string.IsNullOrEmpty(userId)) - { - throw new ArgumentException("Could not retrive 'DbUserId' from config file"); - } - else if (string.IsNullOrEmpty(pass)) - { - throw new ArgumentException("Could not retrive 'DbUserPassword' from config file"); - } - - var dbCredentials = new bnhtrade.Core.Model.Credentials.bnhtradeDB(dataSource, userId, pass); - this._dbCredentials = dbCredentials; - } - catch(Exception ex) - { - throw new Exception("Unable to retirve DB credentials: " + ex.Message); - } - } - - public Connection(Model.Credentials.bnhtradeDB dbCredentials) - { - // setup sql parameters - if (dbCredentials == null) - { - throw new Exception("DB credentials object is null"); - } - this._dbCredentials = dbCredentials; + // build connection string + SqlConnectionString = "Server=" + _server + ";Database=" + _database + ";PersistSecurityInfo=" + _persistSecurityInfo.ToString() + + ";User=" + _user + ";Password=" + _userPassword + ";MultipleActiveResultSets=" + _multipleActiveResultSets.ToString() + + ";ConnectRetryInterval=" + _connectRetryInterval + ";ConnectRetryCount=" + _connectRetryCount + ";Timeout=" + _connectTimeout + + ";Encrypt=" + _encrypt.ToString() +";"; } } } diff --git a/src/bnhtrade.Core/Data/Database/SKU/Price/ReadParameter.cs b/src/bnhtrade.Core/Data/Database/SKU/Price/ReadParameter.cs index 7839832..dc9b15f 100644 --- a/src/bnhtrade.Core/Data/Database/SKU/Price/ReadParameter.cs +++ b/src/bnhtrade.Core/Data/Database/SKU/Price/ReadParameter.cs @@ -10,11 +10,6 @@ namespace bnhtrade.Core.Data.Database.Sku.Price { public class ReadParameter : Connection { - public ReadParameter() : base() - { - - } - public List Execute() { string stringSql = @" diff --git a/src/bnhtrade.Core/Data/Database/ServerPing.cs b/src/bnhtrade.Core/Data/Database/ServerPing.cs new file mode 100644 index 0000000..3eb6a51 --- /dev/null +++ b/src/bnhtrade.Core/Data/Database/ServerPing.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; + +namespace bnhtrade.Core.Data.Database +{ + internal class ServerPing + { + internal ServerPing() + { + } + + internal Logic.Log.LogEvent log = new Logic.Log.LogEvent(); + + /// + /// Polls SQL Server until it comes online, max attempts are reached, or the cancellation token is triggered. + /// + /// Seconds each connection attempt waits before failing. + /// Maximum number of poll attempts. 0 = retry indefinitely. + /// Token to cancel the polling loop. + /// True when the server responds, false if max attempts reached or cancelled. + internal async Task WaitForServerAsync(uint timeout = 10, uint maxAttempts = 6, CancellationToken cancellationToken = default) + { + var connection = new Connection(connectionTimeout: timeout); + + int attempt = 0; + DateTime firstAttempt = DateTime.UtcNow; + int lineWidth = Console.WindowWidth - 1; + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Starting to poll SQL Server for availability..."); + + while (!cancellationToken.IsCancellationRequested) + { + attempt++; + bool isFinalAttempt = maxAttempts > 0 && attempt >= maxAttempts; + + Console.WriteLine($"\r[{DateTime.Now:HH:mm:ss}] Attempt {attempt} of {maxAttempts} "); + + try + { + using SqlConnection conn = new SqlConnection(connection.SqlConnectionString); + if (attempt == 1) + firstAttempt = DateTime.UtcNow; + await conn.OpenAsync(cancellationToken); + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] SQL Server is online, attempt {attempt} succeeded!"); + return true; + } + catch (OperationCanceledException) + { + Console.WriteLine($"\n[{DateTime.Now:HH:mm:ss}] Polling cancelled."); + return false; + } + catch (SqlException ex) + { + if (isFinalAttempt) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] SQL Server not available (error {ex.Number}): {ex.Message}"); + } + } + + if (isFinalAttempt) + { + Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Max attempts ({maxAttempts}) reached, could not connect to SQL Server."); + return false; + } + + // countdown overwrites the same line as the attempt message + int retrySeconds = (firstAttempt.AddSeconds(timeout * attempt) - DateTime.UtcNow).Seconds ; + for (int i = retrySeconds; i > 0; i--) + { + Console.Write($"\r[--------] Retrying in {i}s..."); + try + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + catch (OperationCanceledException) + { + Console.WriteLine($"\n[{DateTime.Now:HH:mm:ss}] Polling cancelled."); + return false; + } + } + } + + return false; + } + } +} diff --git a/src/bnhtrade.Core/Data/Database/UnitOfWork/Connection.cs b/src/bnhtrade.Core/Data/Database/UnitOfWork/Connection.cs deleted file mode 100644 index e4b298c..0000000 --- a/src/bnhtrade.Core/Data/Database/UnitOfWork/Connection.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient; -using System.Configuration; - -namespace bnhtrade.Core.Data.Database.UnitOfWork -{ - public class Connection - { - //protected readonly string SqlConnectionString; - private Model.Credentials.bnhtradeDB _dbCredentials; - - protected string SqlConnectionString - { - get { return _dbCredentials.ConnectionString; } - } - - public Connection() - { - var config = new Config().GetConfiguration(); - - // attempt to retrive credentials from app.local.config - try - { - string dataSource = config.AppSettings.Settings["DbDataSource"].Value; - string userId = config.AppSettings.Settings["DbUserId"].Value; - string pass = config.AppSettings.Settings["DbUserPassword"].Value; - - // check - if (string.IsNullOrEmpty(dataSource)) - { - throw new ArgumentException("Could not retrive 'DbDataSource' from config file"); - } - else if (string.IsNullOrEmpty(userId)) - { - throw new ArgumentException("Could not retrive 'DbUserId' from config file"); - } - else if (string.IsNullOrEmpty(pass)) - { - throw new ArgumentException("Could not retrive 'DbUserPassword' from config file"); - } - - var dbCredentials = new bnhtrade.Core.Model.Credentials.bnhtradeDB(dataSource, userId, pass); - this._dbCredentials = dbCredentials; - } - catch (Exception ex) - { - throw new Exception("Unable to retirve DB credentials: " + ex.Message); - } - } - } -} diff --git a/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypeCrud.cs b/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypeCrud.cs index 163afa4..2f7510c 100644 --- a/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypeCrud.cs +++ b/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypeCrud.cs @@ -9,7 +9,7 @@ using System.Transactions; namespace bnhtrade.Core.Logic.Stock { - public class SkuTransactionTypeCrud : Connection // this inheritance can be removed when old code is removed below + public class SkuTransactionTypeCrud { private Data.Database.Stock.ReadSkuTransactionType dbRead; diff --git a/src/bnhtrade.Core/Logic/Utilities/NightlyRoutine.cs b/src/bnhtrade.Core/Logic/Utilities/NightlyRoutine.cs index a5a843e..0601868 100644 --- a/src/bnhtrade.Core/Logic/Utilities/NightlyRoutine.cs +++ b/src/bnhtrade.Core/Logic/Utilities/NightlyRoutine.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace bnhtrade.Core.Logic.Utilities @@ -14,8 +15,23 @@ namespace bnhtrade.Core.Logic.Utilities { } + public async Task IsServerOnlineAsync() + { + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (sender, e) => { e.Cancel = true; cts.Cancel(); }; + + return await new bnhtrade.Core.Logic.Utilities.SqlServerPing() + .WaitForServerAsync(timeout: 15, maxAttempts: 60, cts.Token); + } + public void DownloadAll() { + if (!IsServerOnlineAsync().GetAwaiter().GetResult()) + { + Console.WriteLine("Server is not online, skipping nightly scheduled tasks."); + return; + } + log.LogInformation("Nightly scheduled tasks started."); bool stockUpdate = false; diff --git a/src/bnhtrade.Core/Logic/Utilities/SqlServerPing.cs b/src/bnhtrade.Core/Logic/Utilities/SqlServerPing.cs new file mode 100644 index 0000000..3762cd6 --- /dev/null +++ b/src/bnhtrade.Core/Logic/Utilities/SqlServerPing.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace bnhtrade.Core.Logic.Utilities +{ + /// + /// Polls SQL Server until it comes online, max attempts are reached, or the operation is cancelled. + /// Intended for use on application startup when the remote SQL Server machine may still be booting. + /// + public class SqlServerPing + { + /// + /// Polls SQL Server until it comes online, max attempts are reached, or the cancellation token is triggered. + /// + /// Seconds each connection attempt waits before failing (1–300). + /// Maximum number of poll attempts. 0 = retry indefinitely. + /// Token to cancel the polling loop. + /// True when the server responds, false if max attempts reached or cancelled. + public async Task WaitForServerAsync(uint timeout = 10, uint maxAttempts = 6, CancellationToken cancellationToken = default) + { + return await new Data.Database.ServerPing().WaitForServerAsync(timeout, maxAttempts, cancellationToken); + } + } +} diff --git a/src/bnhtrade.Core/Model/Credentials/AmazonSPAPI.cs b/src/bnhtrade.Core/Model/Credentials/AmazonSPAPI.cs deleted file mode 100644 index 2d90beb..0000000 --- a/src/bnhtrade.Core/Model/Credentials/AmazonSPAPI.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace bnhtrade.Core.Model.Credentials -{ - public class AmazonSPAPI - { - public string AccessKey { get; private set; } - public string SecretKey { get; private set; } - public string RoleArn { get; private set; } - public string ClientId { get; private set; } - public string ClientSecret { get; private set; } - public string RefreshToken { get; private set; } - - public AmazonSPAPI(string accessKey, string secretKey, string roleArn, string clientId, string clientSecret, string refreshToken) - { - this.AccessKey = accessKey; - this.SecretKey = secretKey; - this.RoleArn = roleArn; - this.ClientId = clientId; - this.ClientSecret = clientSecret; - this.RefreshToken = refreshToken; - } - } -} diff --git a/src/bnhtrade.Core/Model/Credentials/bnhtradeDB.cs b/src/bnhtrade.Core/Model/Credentials/bnhtradeDB.cs deleted file mode 100644 index 5fed5f3..0000000 --- a/src/bnhtrade.Core/Model/Credentials/bnhtradeDB.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace bnhtrade.Core.Model.Credentials -{ - public class bnhtradeDB - { - public string DataSource { get; private set; } - - public string UserId { get; private set; } - - public string UserPassword { get; private set; } - - public string InitialCatalog { get; private set; } = "e2A"; - - public bool PersistSecurityInfo { get; private set; } = true; - - public bool MultipleActiveResultSets { get; private set; } = true; - - public uint ConnectionTimeout { get; private set; } - - public string ConnectionString - { - get - { - return "Data Source=" + DataSource + ";Initial Catalog=" + InitialCatalog + ";Persist Security Info=" + PersistSecurityInfo.ToString() - + ";User ID=" + UserId + ";Password=" + UserPassword + ";MultipleActiveResultSets=" + MultipleActiveResultSets.ToString() - + ";Connect Timeout=" + ConnectionTimeout + ";Encrypt=True"; - } - } - - public bnhtradeDB (string source, string userId, string userPassword, uint connectionTimeout = 30) - { - this.DataSource = source; - this.UserId = userId; - this.UserPassword = userPassword; - this.ConnectionTimeout = connectionTimeout; - } - } -} diff --git a/src/bnhtrade.ScheduledTasks/Program.cs b/src/bnhtrade.ScheduledTasks/Program.cs index 890d547..77a3474 100644 --- a/src/bnhtrade.ScheduledTasks/Program.cs +++ b/src/bnhtrade.ScheduledTasks/Program.cs @@ -2,6 +2,7 @@ using System; using System.Configuration; using System.Threading; +using System.Threading.Tasks; using System.Transactions; namespace bnhtradeScheduledTasks @@ -12,7 +13,7 @@ namespace bnhtradeScheduledTasks { } - static void Main(string[] args) + static async Task Main(string[] args) { if (OperatingSystem.IsWindows()) { @@ -310,7 +311,7 @@ namespace bnhtradeScheduledTasks Console.WriteLine(consoleHeader); Console.WriteLine("Main Menu > Dev Funcions"); Console.WriteLine(); - Console.WriteLine("<1> Test some randon function I've set here"); + Console.WriteLine("<1> Ping SQL Server"); Console.WriteLine("<2> Test Account"); Console.WriteLine("<3> Test Export"); Console.WriteLine("<4> Test Import"); @@ -334,7 +335,16 @@ namespace bnhtradeScheduledTasks { Console.Clear(); - var obj = new bnhtrade.Core.Test.Amazon.SP_API.VariousCalls(); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (sender, e) => { e.Cancel = true; cts.Cancel(); }; + + bool online = await new bnhtrade.Core.Logic.Utilities.SqlServerPing() + .WaitForServerAsync(timeout: 10, maxAttempts: 10, cts.Token); + + if (!online) + { + Console.WriteLine("Could not connect to SQL Server."); + } Console.WriteLine("Complete, press any key to continue..."); Console.ReadKey();