From 14f6df61f26c762b02fe23020cd150bf68e9c42a Mon Sep 17 00:00:00 2001 From: ecosSystem Date: Tue, 27 Oct 2026 14:15:18 +0100 Subject: [PATCH] SQLRDD: refresh the lock table on a connection of its own Every connection starts a System.Timers.Timer that refreshes its entries in the lock table every 120 seconds. The timer runs on a thread pool thread but used the connection of the application, and an ADO.NET connection must not be used by two threads at once. With short statements this rarely collided; importing a large dataset it did reliably: a refresh during a CREATE INDEX on a 1.1 million row table made the index fail with "ExecuteNonQuery requires an open and available Connection", and a following metadata query returned nothing. The refresh now opens a connection of its own (pooled), and it is skipped when UseLocks is off, as nothing is written to the lock table then. Errors of the refresh no longer escape the timer thread. Co-Authored-By: Claude Opus 5.5 --- .../XSharp.SQLRdd/Classes/Connection.prg | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/Runtime/XSharp.SQLRdd/Classes/Connection.prg b/src/Runtime/XSharp.SQLRdd/Classes/Connection.prg index 9dc87680fd..0cfe9289ad 100644 --- a/src/Runtime/XSharp.SQLRdd/Classes/Connection.prg +++ b/src/Runtime/XSharp.SQLRdd/Classes/Connection.prg @@ -1045,33 +1045,50 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable _lockTimer:Enabled := true return + /// + /// The timer runs on a thread pool thread, while the application uses the connection on its own + /// thread. An ADO.NET connection must not be used by two threads at once: a refresh that came + /// during a longer statement (e.g. CREATE INDEX on a big table) made one of them fail with + /// "requires an open and available Connection". So the refresh uses a connection of its own. + /// method LockTimerElapsedEvent(sender as object, e as System.Timers.ElapsedEventArgs) as void - var parameterName1 := SELF:Provider:ParameterPrefix + "p1" - - // Refresh my Locks - using var cmdRefresh := SqlDbCommand{"RefreshLockTable", SELF, false} - if !cmdRefresh:Connection:IsOpen - cmdRefresh:Connection:ForceOpen() - endif - var updateStatement := SELF:Provider:UpdateStatement:Replace(SqlDbProvider.TableNameMacro, LockTableName) - updateStatement := updateStatement:Replace(SqlDbProvider.ColumnsMacro, "lockdatetime = " + SELF:Provider:CurrentDateTime) - updateStatement := updateStatement:Replace(SqlDbProvider.WhereMacro, "connectionid = " + parameterName1) - cmdRefresh:Parameters := List{} - cmdRefresh:Parameters:Add(SqlDbParameter{parameterName1, SELF:ConnectionId:ToString()}) - cmdRefresh:CommandText := updateStatement - cmdRefresh:ExecuteNonQuery() - - // Clear all old locks - using var cmdClear := SqlDbCommand{"ClearLockTable", SELF, false} - if !cmdClear:Connection:IsOpen - cmdClear:Connection:ForceOpen() + if !SELF:UseLocks + // nothing is written to the lock table + return endif - cmdClear:CommandText := SELF:Provider:DeleteStatement:Replace(SqlDbProvider.TableNameMacro, LockTableName):Replace(SqlDbProvider.WhereMacro, "LockDateTime < " + parameterName1) - cmdClear:Parameters := List{} - cmdClear:Parameters:Add(SqlDbParameter{parameterName1, DateTime.Now.AddSeconds(-SELF:LockStaleThreshold)}) - cmdClear:ExecuteNonQuery() + try + var parameterName1 := SELF:Provider:ParameterPrefix + "p1" + using var oConn := SELF:Provider:CreateConnection() + oConn:ConnectionString := SELF:ConnectionString + oConn:Open() + + // Refresh my Locks + using var cmdRefresh := oConn:CreateCommand() + var updateStatement := SELF:Provider:UpdateStatement:Replace(SqlDbProvider.TableNameMacro, LockTableName) + updateStatement := updateStatement:Replace(SqlDbProvider.ColumnsMacro, "lockdatetime = " + SELF:Provider:CurrentDateTime) + updateStatement := updateStatement:Replace(SqlDbProvider.WhereMacro, "connectionid = " + parameterName1) + cmdRefresh:CommandText := updateStatement + SELF:_AddParameter(cmdRefresh, parameterName1, SELF:ConnectionId:ToString()) + cmdRefresh:ExecuteNonQuery() + + // Clear all old locks + using var cmdClear := oConn:CreateCommand() + cmdClear:CommandText := SELF:Provider:DeleteStatement:Replace(SqlDbProvider.TableNameMacro, LockTableName):Replace(SqlDbProvider.WhereMacro, "LockDateTime < " + parameterName1) + SELF:_AddParameter(cmdClear, parameterName1, DateTime.Now.AddSeconds(-SELF:LockStaleThreshold)) + cmdClear:ExecuteNonQuery() + catch ex as Exception + // a failed refresh must not take the application down; the next tick tries again + System.Diagnostics.Debug.WriteLine("SQLRDD lock table refresh: " + ex:Message) + end try return + private method _AddParameter(oCmd as DbCommand, cName as string, oValue as object) as void + var oParam := SELF:Provider:CreateParameter() + oParam:ParameterName := cName + oParam:Value := oValue + oCmd:Parameters:Add(oParam) + return + #endregion end class