Skip to content

Commit ee12f78

Browse files
NH-4084 - DbTimeStamp cause stale update exception
1 parent 1bfa978 commit ee12f78

File tree

2 files changed

+85
-91
lines changed

2 files changed

+85
-91
lines changed

src/NHibernate/Async/Type/DbTimestampType.cs

Lines changed: 48 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using System;
1212
using System.Data;
1313
using System.Data.Common;
14-
1514
using NHibernate.Engine;
1615
using NHibernate.Exceptions;
1716
using NHibernate.Impl;
@@ -26,88 +25,76 @@ public partial class DbTimestampType : TimestampType
2625
{
2726

2827
/// <inheritdoc />
29-
public override Task<object> SeedAsync(ISessionImplementor session, CancellationToken cancellationToken)
28+
public override async Task<object> SeedAsync(ISessionImplementor session, CancellationToken cancellationToken)
3029
{
31-
if (cancellationToken.IsCancellationRequested)
30+
cancellationToken.ThrowIfCancellationRequested();
31+
if (session == null)
3232
{
33-
return Task.FromCanceled<object>(cancellationToken);
33+
log.Debug("incoming session was null; using current vm time");
34+
return await (base.SeedAsync(null, cancellationToken)).ConfigureAwait(false);
3435
}
35-
try
36+
else if (!session.Factory.Dialect.SupportsCurrentTimestampSelection)
3637
{
37-
if (session == null)
38-
{
39-
log.Debug("incoming session was null; using current vm time");
40-
return base.SeedAsync(null, cancellationToken);
41-
}
42-
else if (!session.Factory.Dialect.SupportsCurrentTimestampSelection)
43-
{
44-
log.Debug("falling back to vm-based timestamp, as dialect does not support current timestamp selection");
45-
return base.SeedAsync(session, cancellationToken);
46-
}
47-
else
48-
{
49-
return GetCurrentTimestampAsync(session, cancellationToken);
50-
}
38+
log.Debug("falling back to vm-based timestamp, as dialect does not support current timestamp selection");
39+
return await (base.SeedAsync(session, cancellationToken)).ConfigureAwait(false);
5140
}
52-
catch (Exception ex)
41+
else
5342
{
54-
return Task.FromException<object>(ex);
43+
return await (GetCurrentTimestampAsync(session, cancellationToken)).ConfigureAwait(false);
5544
}
5645
}
5746

58-
private Task<object> GetCurrentTimestampAsync(ISessionImplementor session, CancellationToken cancellationToken)
47+
protected virtual async Task<DateTime> GetCurrentTimestampAsync(ISessionImplementor session, CancellationToken cancellationToken)
5948
{
60-
if (cancellationToken.IsCancellationRequested)
61-
{
62-
return Task.FromCanceled<object>(cancellationToken);
63-
}
64-
try
65-
{
66-
Dialect.Dialect dialect = session.Factory.Dialect;
67-
string timestampSelectString = dialect.CurrentTimestampSelectString;
68-
return UsePreparedStatementAsync(timestampSelectString, session, cancellationToken);
69-
}
70-
catch (Exception ex)
71-
{
72-
return Task.FromException<object>(ex);
73-
}
49+
cancellationToken.ThrowIfCancellationRequested();
50+
var dialect = session.Factory.Dialect;
51+
// Need to round notably for Sql Server DateTime with Odbc, which has a 3.33ms resolution,
52+
// causing stale data update failure 2/3 of times if not rounded to 10ms.
53+
return Round(
54+
await (UsePreparedStatementAsync(dialect.CurrentTimestampSelectString, session, cancellationToken)).ConfigureAwait(false),
55+
dialect.TimestampResolutionInTicks);
7456
}
7557

76-
protected virtual async Task<object> UsePreparedStatementAsync(string timestampSelectString, ISessionImplementor session, CancellationToken cancellationToken)
58+
protected virtual async Task<DateTime> UsePreparedStatementAsync(string timestampSelectString, ISessionImplementor session, CancellationToken cancellationToken)
7759
{
7860
cancellationToken.ThrowIfCancellationRequested();
7961
var tsSelect = new SqlString(timestampSelectString);
8062
DbCommand ps = null;
8163
DbDataReader rs = null;
82-
using (new SessionIdLoggingContext(session.SessionId))
83-
try
84-
{
85-
ps = await (session.Batcher.PrepareCommandAsync(CommandType.Text, tsSelect, EmptyParams, cancellationToken)).ConfigureAwait(false);
86-
rs = await (session.Batcher.ExecuteReaderAsync(ps, cancellationToken)).ConfigureAwait(false);
87-
await (rs.ReadAsync(cancellationToken)).ConfigureAwait(false);
88-
DateTime ts = rs.GetDateTime(0);
89-
if (log.IsDebugEnabled)
90-
{
91-
log.Debug("current timestamp retreived from db : " + ts + " (tiks=" + ts.Ticks + ")");
92-
}
93-
return ts;
94-
}
95-
catch (DbException sqle)
64+
using (new SessionIdLoggingContext(session.SessionId))
9665
{
97-
throw ADOExceptionHelper.Convert(session.Factory.SQLExceptionConverter, sqle,
98-
"could not select current db timestamp", tsSelect);
99-
}
100-
finally
101-
{
102-
if (ps != null)
66+
try
10367
{
104-
try
68+
ps = await (session.Batcher.PrepareCommandAsync(CommandType.Text, tsSelect, EmptyParams, cancellationToken)).ConfigureAwait(false);
69+
rs = await (session.Batcher.ExecuteReaderAsync(ps, cancellationToken)).ConfigureAwait(false);
70+
await (rs.ReadAsync(cancellationToken)).ConfigureAwait(false);
71+
var ts = rs.GetDateTime(0);
72+
if (log.IsDebugEnabled)
10573
{
106-
session.Batcher.CloseCommand(ps, rs);
74+
log.Debug("current timestamp retreived from db : " + ts + " (tiks=" + ts.Ticks + ")");
10775
}
108-
catch (DbException sqle)
76+
return ts;
77+
}
78+
catch (DbException sqle)
79+
{
80+
throw ADOExceptionHelper.Convert(
81+
session.Factory.SQLExceptionConverter,
82+
sqle,
83+
"could not select current db timestamp",
84+
tsSelect);
85+
}
86+
finally
87+
{
88+
if (ps != null)
10989
{
110-
log.Warn("unable to clean up prepared statement", sqle);
90+
try
91+
{
92+
session.Batcher.CloseCommand(ps, rs);
93+
}
94+
catch (DbException sqle)
95+
{
96+
log.Warn("unable to clean up prepared statement", sqle);
97+
}
11198
}
11299
}
113100
}

src/NHibernate/Type/DbTimestampType.cs

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Data;
33
using System.Data.Common;
4-
54
using NHibernate.Engine;
65
using NHibernate.Exceptions;
76
using NHibernate.Impl;
@@ -46,47 +45,55 @@ public override object Seed(ISessionImplementor session)
4645
}
4746
}
4847

49-
private object GetCurrentTimestamp(ISessionImplementor session)
48+
protected virtual DateTime GetCurrentTimestamp(ISessionImplementor session)
5049
{
51-
Dialect.Dialect dialect = session.Factory.Dialect;
52-
string timestampSelectString = dialect.CurrentTimestampSelectString;
53-
return UsePreparedStatement(timestampSelectString, session);
50+
var dialect = session.Factory.Dialect;
51+
// Need to round notably for Sql Server DateTime with Odbc, which has a 3.33ms resolution,
52+
// causing stale data update failure 2/3 of times if not rounded to 10ms.
53+
return Round(
54+
UsePreparedStatement(dialect.CurrentTimestampSelectString, session),
55+
dialect.TimestampResolutionInTicks);
5456
}
5557

56-
protected virtual object UsePreparedStatement(string timestampSelectString, ISessionImplementor session)
58+
protected virtual DateTime UsePreparedStatement(string timestampSelectString, ISessionImplementor session)
5759
{
5860
var tsSelect = new SqlString(timestampSelectString);
5961
DbCommand ps = null;
6062
DbDataReader rs = null;
61-
using (new SessionIdLoggingContext(session.SessionId))
62-
try
63+
using (new SessionIdLoggingContext(session.SessionId))
6364
{
64-
ps = session.Batcher.PrepareCommand(CommandType.Text, tsSelect, EmptyParams);
65-
rs = session.Batcher.ExecuteReader(ps);
66-
rs.Read();
67-
DateTime ts = rs.GetDateTime(0);
68-
if (log.IsDebugEnabled)
65+
try
6966
{
70-
log.Debug("current timestamp retreived from db : " + ts + " (tiks=" + ts.Ticks + ")");
71-
}
72-
return ts;
73-
}
74-
catch (DbException sqle)
75-
{
76-
throw ADOExceptionHelper.Convert(session.Factory.SQLExceptionConverter, sqle,
77-
"could not select current db timestamp", tsSelect);
78-
}
79-
finally
80-
{
81-
if (ps != null)
82-
{
83-
try
67+
ps = session.Batcher.PrepareCommand(CommandType.Text, tsSelect, EmptyParams);
68+
rs = session.Batcher.ExecuteReader(ps);
69+
rs.Read();
70+
var ts = rs.GetDateTime(0);
71+
if (log.IsDebugEnabled)
8472
{
85-
session.Batcher.CloseCommand(ps, rs);
73+
log.Debug("current timestamp retreived from db : " + ts + " (tiks=" + ts.Ticks + ")");
8674
}
87-
catch (DbException sqle)
75+
return ts;
76+
}
77+
catch (DbException sqle)
78+
{
79+
throw ADOExceptionHelper.Convert(
80+
session.Factory.SQLExceptionConverter,
81+
sqle,
82+
"could not select current db timestamp",
83+
tsSelect);
84+
}
85+
finally
86+
{
87+
if (ps != null)
8888
{
89-
log.Warn("unable to clean up prepared statement", sqle);
89+
try
90+
{
91+
session.Batcher.CloseCommand(ps, rs);
92+
}
93+
catch (DbException sqle)
94+
{
95+
log.Warn("unable to clean up prepared statement", sqle);
96+
}
9097
}
9198
}
9299
}

0 commit comments

Comments
 (0)