Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Runtime/XSharp.SQLRdd/Classes/Command.prg
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class SqlDbCommand inherit SqlDbHandleObject implements IDisposable
/// <seealso cref="SqlDbConnection.LastException"/>
method GetSchemaTable() as DataTable
try
self:Connection:ForceOpen()
if ! SELF:_TryBindParameters()
return null
endif
Expand Down Expand Up @@ -150,6 +151,7 @@ class SqlDbCommand inherit SqlDbHandleObject implements IDisposable
/// <seealso cref="SqlDbConnection.LastException"/>
method ExecuteScalar(cTable := "" as string) as object
try
self:Connection:ForceOpen()
if String.IsNullOrEmpty(cTable)
cTable := SELF:Name
endif
Expand All @@ -176,6 +178,7 @@ class SqlDbCommand inherit SqlDbHandleObject implements IDisposable
/// <seealso cref="SqlDbConnection.LastException"/>
method ExecuteReader(cTable := "" as string) as DbDataReader
try
self:Connection:ForceOpen()
if String.IsNullOrEmpty(cTable)
cTable := SELF:Name
endif
Expand All @@ -202,6 +205,7 @@ class SqlDbCommand inherit SqlDbHandleObject implements IDisposable
/// <seealso cref="SqlDbConnection.LastException"/>
method ExecuteNonQuery(cTable := "" as string) as LOGIC
try
self:Connection:ForceOpen()
if String.IsNullOrEmpty(cTable)
cTable := SELF:Name
endif
Expand Down
115 changes: 98 additions & 17 deletions src/Runtime/XSharp.SQLRdd/Classes/Connection.prg
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
private _metadataCollections as List<string>
private _databaseRestrictions as int
private _tableRestrictions as int
// Must be kept in a field: a System.Timers.Timer with no other root gets garbage
// collected (it stops firing silently), which would disable the periodic stale-lock cleanup.
private _lockTimer as System.Timers.Timer

#region Connection Only Properties
/// <summary>Dictionary with properties defined by the Ado.Net provider</summary>
Expand Down Expand Up @@ -74,7 +77,17 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
/// <summary>Provider for the Metadata, such as columnlist, maxrecords etc.</summary>
property MetadataProvider as ISqlMetadataProvider auto
/// <summary>Last exception that occurred in the RDD</summary>
property LastException as Exception auto get internal set
property LastException as Exception
get
return _lastException
end get
internal set
_lastException := value
if value != null
System.Diagnostics.Trace.WriteLine(String.Format("SqlDbConnection '{0}': {1}", self:Name, value:ToString()))
endif
end set
end property
/// <summary>Connection State</summary>
PROPERTY State as ConnectionState get iif(self:DbConnection == null, ConnectionState.Closed, self:DbConnection:State)

Expand Down Expand Up @@ -264,6 +277,8 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
BufferSize := DEFAULT_BUFFERSIZE
DeletedColumn := DEFAULT_DELETEDCOLUMN
RecnoColumn := DEFAULT_RECNOCOLUMN
LockRefreshInterval:= DEFAULT_LOCKREFRESHINTERVAL
LockStaleThreshold := DEFAULT_LOCKSTALETHRESHOLD
cConnectionString := self:AnalyzeConnectionString(cConnectionString)
self:ConnectionString := cConnectionString
DbConnection := Provider:CreateConnection()
Expand All @@ -284,6 +299,9 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
SELF:_FillDataSourceProperties()
SELF:_CreateLockTable()
SELF:InitializeLockTimer()
// Sweep stale locks (e.g. left behind by a crashed/killed previous process) right away,
// instead of waiting for the first 120-second timer tick.
SELF:LockTimerElapsedEvent(null, null)
// Todo: Check for # of open users and close the connection when no users are left and then throw an exception
return
end constructor
Expand All @@ -299,6 +317,11 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
if self:RDDs:Count > 0
return false
endif
if _lockTimer != null
_lockTimer:Enabled := false
_lockTimer:Dispose()
_lockTimer := null
endif
// Logout the workstation from the Open Connections table
if _command != null
_command:Dispose()
Expand Down Expand Up @@ -368,7 +391,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
return false
endif
catch e as Exception
_lastException := e
self:LastException := e
self:DbTransaction := null
end try
return self:DbTransaction != null
Expand All @@ -388,7 +411,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
return false
endif
catch e as Exception
_lastException := e
self:LastException := e
self:DbTransaction := null
end try
return self:DbTransaction != null
Expand Down Expand Up @@ -447,7 +470,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
_command:CommandText := cCommand
result := _command:ExecuteScalar(cTable)
catch e as Exception
_lastException := e
self:LastException := e
result := null
end try
return result
Expand All @@ -465,7 +488,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
_command:CommandText := cCommand
result := _command:ExecuteNonQuery(cTable)
catch e as Exception
_lastException := e
self:LastException := e
result := false
end try
return result
Expand All @@ -483,7 +506,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
_command:CommandText := cCommand
result := _command:ExecuteReader(cTable)
catch e as Exception
_lastException := e
self:LastException := e
result := null
end try
return result
Expand All @@ -501,7 +524,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
_command:CommandText := cCommand
result := _command:GetDataTable(cTable)
catch e as Exception
_lastException := e
self:LastException := e
result := null
end try
return result
Expand Down Expand Up @@ -541,6 +564,26 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
var fieldNames := List<string>{}
foreach row as DataRow in schema:Rows
local colInfo := SQLHelpers.GetColumnInfoFromSchemaRow(row, fieldNames, longFieldNames) as DbColumnInfo
if colInfo:FieldType == DbFieldType.DateTime
// SQLHelpers.GetColumnInfo() tells DBF "D" (Date) apart from "T" (DateTime)
// purely by NumericPrecision, but a plain SQL Server `date` column reports
// the very same "not applicable" sentinel precision (255, via
// System.Data.SqlClient) as `datetime2` does, so it always fails that check
// and comes back as "T" - a Date column can never round-trip correctly.
// NumericScale tells them apart reliably instead: any genuine time-bearing
// column (datetime/datetime2/smalldatetime, whatever fractional-seconds
// precision) reports a real, small scale (0-7), while a `date` column keeps
// the 255 sentinel there too - so an implausibly high scale means "no time
// component", i.e. this is really a Date.
local nScale := 0 as short
if row:Table:Columns:Contains("NumericScale") .and. row["NumericScale"] is short var nScaleVal
nScale := nScaleVal
endif
if nScale >= 100
colInfo:FieldType := DbFieldType.Date
colInfo:Length := 8
endif
endif
if SELF:LegacyFieldTypes
// Map back to the old field types
switch colInfo:FieldType
Expand Down Expand Up @@ -583,7 +626,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
next
return oTd
catch e as Exception
_lastException := e
self:LastException := e
end try
return null
end method
Expand Down Expand Up @@ -613,7 +656,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
self:Schema:Add(TableName, oTd)
return oTd
catch e as Exception
_lastException := e
self:LastException := e
end try
return null
end method
Expand All @@ -630,6 +673,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
// DROP TABLE) causes DoesTableExist() to keep reporting a just-dropped table as
// existing, which then skips re-creating it.
try
self:ForceOpen()
if !SELF:HasCollection(TABLECOLLECTION)
return false
endif
Expand All @@ -650,7 +694,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
next
endif
catch e as Exception
_lastException := e
self:LastException := e
end try
return false
end method
Expand All @@ -662,6 +706,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
/// <param name="cDatabase">Name of the database to check</param>
method DoesDatabaseExist(cDatabase as string) as logic
try
self:ForceOpen()
if !SELF:HasCollection(DATABASECOLLECTION)
return false
endif
Expand All @@ -682,7 +727,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
next
endif
catch e as Exception
_lastException := e
self:LastException := e
end try
return false
end method
Expand All @@ -693,6 +738,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
/// <returns>List of table names that match the filter</returns>
method GetTables(filter := "" as string) as List<string>
try
self:ForceOpen()
var result := List<string>{}
if self:HasCollection(TABLECOLLECTION)
var dt := self:DbConnection:GetSchema(TABLECOLLECTION)
Expand All @@ -709,7 +755,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
endif
return result
catch e as Exception
_lastException := e
self:LastException := e
end try
return null
end method
Expand All @@ -719,6 +765,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
/// </summary>
/// <returns>List of metadata collections</returns>
method GetMetaDataCollections() as List<string>
self:ForceOpen()
var dt := self:DbConnection:GetSchema(DbMetaDataCollectionNames.MetaDataCollections)
var result := List<string>{}
foreach row as DataRow in dt:Rows
Expand All @@ -737,6 +784,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
/// </summary>
/// <returns>List of metadata collections</returns>
method GetMetaDataCollection(cCollection as string) as DataTable
self:ForceOpen()
var dt := self:DbConnection:GetSchema(cCollection)
return dt
end method
Expand All @@ -751,6 +799,39 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable

#endregion

// Interval at which a connection refreshes the timestamp on its own locks, and the age at
// which another connection's lock is considered abandoned and cleared. The threshold must
// stay comfortably above the refresh interval: with no margin, a lock could be judged
// abandoned just before its owner's own refresh tick runs, e.g. under GC/scheduler jitter,
// and a still-active user on another workstation (or another instance) would lose their lock.
// Both are overridable per connection, via ini or callback, same as PageSize/BufferSize -
// see SqlRDDEventReason.LockRefreshInterval/LockStaleThreshold and Metadata/Abstract.prg.
INTERNAL CONST DEFAULT_LOCKREFRESHINTERVAL := 120 AS INT
INTERNAL CONST DEFAULT_LOCKSTALETHRESHOLD := 600 AS INT

/// <summary>
/// The interval (in seconds) at which this connection refreshes the timestamp on its own
/// locks and sweeps locks older than <see cref="LockStaleThreshold"/> (also in seconds).
/// </summary>
property LockRefreshInterval as int
get
return _lockRefreshInterval
end get
set
_lockRefreshInterval := value
if _lockTimer != null
_lockTimer:Interval := value * 1000
endif
end set
end property
private _lockRefreshInterval as int

/// <summary>
/// The age (in seconds) after which another connection's lock is considered abandoned and
/// cleared. Must stay comfortably above <see cref="LockRefreshInterval"/> (see remarks above).
/// </summary>
property LockStaleThreshold as int auto

INTERNAL CONST DEFAULT_ALLOWUPDATES := TRUE AS LOGIC
INTERNAL CONST DEFAULT_COMPAREMEMO := TRUE AS LOGIC
INTERNAL CONST DEFAULT_DELETEDCOLUMN := "" AS STRING
Expand Down Expand Up @@ -854,10 +935,10 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
end method

private method InitializeLockTimer() as void
var timer := System.Timers.Timer{120000} // 120 sec
timer:Elapsed += System.Timers.ElapsedEventHandler{ @@LockTimerElapsedEvent }
timer:AutoReset := true
timer:Enabled := true
_lockTimer := System.Timers.Timer{SELF:LockRefreshInterval * 1000}
_lockTimer:Elapsed += System.Timers.ElapsedEventHandler{ @@LockTimerElapsedEvent }
_lockTimer:AutoReset := true
_lockTimer:Enabled := true
return

method LockTimerElapsedEvent(sender as object, e as System.Timers.ElapsedEventArgs) as void
Expand All @@ -883,7 +964,7 @@ class SqlDbConnection inherit SqlDbHandleObject implements IDisposable
endif
cmdClear:CommandText := SELF:Provider:DeleteStatement:Replace(SqlDbProvider.TableNameMacro, LockTableName):Replace(SqlDbProvider.WhereMacro, "LockDateTime < " + parameterName1)
cmdClear:Parameters := List<SqlDbParameter>{}
cmdClear:Parameters:Add(SqlDbParameter{parameterName1, DateTime.Now.AddSeconds(-120)})
cmdClear:Parameters:Add(SqlDbParameter{parameterName1, DateTime.Now.AddSeconds(-SELF:LockStaleThreshold)})
cmdClear:ExecuteNonQuery()
return

Expand Down
2 changes: 2 additions & 0 deletions src/Runtime/XSharp.SQLRdd/Metadata/Abstract.prg
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ ABSTRACT CLASS SqlMetadataProviderAbstract IMPLEMENTS ISqlMetadataProvider
_connection:AllowUpdates := SELF:GetLogic(oPar, SqlRDDEventReason.AllowUpdates, _connection:AllowUpdates)
_connection:PageSize := SELF:GetInt(oPar, SqlRDDEventReason.PageSize, _connection:PageSize)
_connection:BufferSize := SELF:GetInt(oPar, SqlRDDEventReason.BufferSize, _connection:BufferSize)
_connection:LockRefreshInterval := SELF:GetInt(oPar, SqlRDDEventReason.LockRefreshInterval, _connection:LockRefreshInterval)
_connection:LockStaleThreshold := SELF:GetInt(oPar, SqlRDDEventReason.LockStaleThreshold, _connection:LockStaleThreshold)
_connection:RecnoColumn := SELF:GetString(oPar, SqlRDDEventReason.RecnoColumn, _connection:RecnoColumn)
_connection:DeletedColumn := SELF:GetString(oPar, SqlRDDEventReason.DeletedColumn, _connection:DeletedColumn)
_connection:CompareMemo := SELF:GetLogic(oPar, SqlRDDEventReason.CompareMemo, _connection:CompareMemo)
Expand Down
Loading