Skip to content

Commit cfa8cf7

Browse files
authored
1223: Warn when web and database server time differ (#7815)
1 parent 814cdb0 commit cfa8cf7

4 files changed

Lines changed: 134 additions & 49 deletions

File tree

api/src/org/labkey/api/data/dialect/SqlDialect.java

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.labkey.api.module.ModuleLoader;
6060
import org.labkey.api.query.FieldKey;
6161
import org.labkey.api.util.ExceptionUtil;
62+
import org.labkey.api.util.HtmlString;
6263
import org.labkey.api.util.MemTracker;
6364
import org.labkey.api.util.StringUtilsLabKey;
6465
import org.labkey.api.util.SystemMaintenance;
@@ -80,6 +81,8 @@
8081
import java.sql.SQLException;
8182
import java.sql.Statement;
8283
import java.sql.Types;
84+
import java.time.Duration;
85+
import java.time.LocalDateTime;
8386
import java.util.ArrayList;
8487
import java.util.Arrays;
8588
import java.util.Calendar;
@@ -95,6 +98,7 @@
9598
import java.util.regex.Pattern;
9699

97100
import static org.junit.Assert.assertEquals;
101+
import static org.junit.Assert.assertFalse;
98102
import static org.junit.Assert.assertThrows;
99103
import static org.junit.Assert.assertTrue;
100104

@@ -168,7 +172,7 @@ public String getOtherDatabaseThreads()
168172
{
169173
StringBuilder sb = new StringBuilder();
170174

171-
// Per 18789, also include threads without db connections.
175+
// Per Issue 18789, also include threads without db connections.
172176
List<Thread> dbThreads = new ArrayList<>();
173177

174178
for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet())
@@ -863,25 +867,25 @@ public SQLFragment getNumericCast(SQLFragment expression)
863867
* @param arguments Arguments passed from the LK SQL
864868
* @return the dialect equivalent SQLFragrment
865869
*/
866-
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
867-
{
868-
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
869-
}
870-
871-
public boolean supportsIsNumeric()
872-
{
873-
return false;
874-
}
875-
876-
public SQLFragment isNumericExpr(SQLFragment expression)
877-
{
878-
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
879-
}
880-
881-
public void handleCreateDatabaseException(SQLException e) throws ServletException
882-
{
883-
throw(new ServletException("Can't create database", e));
884-
}
870+
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
871+
{
872+
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
873+
}
874+
875+
public boolean supportsIsNumeric()
876+
{
877+
return false;
878+
}
879+
880+
public SQLFragment isNumericExpr(SQLFragment expression)
881+
{
882+
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
883+
}
884+
885+
public void handleCreateDatabaseException(SQLException e) throws ServletException
886+
{
887+
throw(new ServletException("Can't create database", e));
888+
}
885889

886890
/**
887891
* Wrap one or more INSERT statements to allow explicit specification
@@ -1990,12 +1994,61 @@ public final Collection<String> getExecutionPlan(DbScope scope, SQLFragment sql,
19901994
// Add any database configuration warnings (e.g., missing aggregate function or deprecated database server version)
19911995
// to display in the page header for administrators. This will be called:
19921996
// - Only on the LabKey DataSource's dialect instance (not external data sources)
1993-
// - After the core module has been upgraded and the dialect has been prepared for the last time, meaning the dialect
1997+
// - After the core module has been upgraded, and the dialect has been prepared for the last time, meaning the dialect
19941998
// should reflect the final database configuration
19951999
public void addAdminWarningMessages(Warnings warnings, boolean showAllWarnings)
19962000
{
19972001
}
19982002

2003+
public static final long TIME_DIFFERENCE_WARNING_SECONDS = 10;
2004+
2005+
public static ServerDatabaseTimeDifference getServerDatabaseTimeDifference(DbScope scope)
2006+
{
2007+
// Compare LocalDateTime to capture any difference in server and database times (skew or timezone).
2008+
LocalDateTime serverTime = LocalDateTime.now();
2009+
LocalDateTime databaseTime = new SqlSelector(scope, "SELECT CURRENT_TIMESTAMP").getObject(LocalDateTime.class);
2010+
2011+
return new ServerDatabaseTimeDifference(serverTime, databaseTime);
2012+
}
2013+
2014+
public record ServerDatabaseTimeDifference(LocalDateTime serverTime, LocalDateTime databaseTime)
2015+
{
2016+
public long getSeconds()
2017+
{
2018+
return Math.abs(Duration.between(serverTime, databaseTime).toSeconds());
2019+
}
2020+
2021+
public boolean exceedsWarningThreshold()
2022+
{
2023+
return getSeconds() > TIME_DIFFERENCE_WARNING_SECONDS;
2024+
}
2025+
}
2026+
2027+
// GH Issue #1223: Add a site configuration warning for administrators if the server and database clocks differ.
2028+
protected void addTimeDifferenceWarning(Warnings warnings, boolean showAllWarnings)
2029+
{
2030+
try
2031+
{
2032+
ServerDatabaseTimeDifference difference = getServerDatabaseTimeDifference(DbScope.getLabKeyScope());
2033+
2034+
if (difference.exceedsWarningThreshold())
2035+
warnings.add(getTimeDifferenceWarning(difference.getSeconds()));
2036+
else if (showAllWarnings)
2037+
warnings.add(getTimeDifferenceWarning(TIME_DIFFERENCE_WARNING_SECONDS + 1));
2038+
}
2039+
catch (Exception e)
2040+
{
2041+
LOG.warn("Unable to compare web server and database server times", e);
2042+
}
2043+
}
2044+
2045+
private HtmlString getTimeDifferenceWarning(long seconds)
2046+
{
2047+
return HtmlString.of("The web server and database server times differ by " + seconds + " seconds. " +
2048+
"LabKey Server often relies on comparing timestamps stored in the database with timestamps generated by " +
2049+
"the web server, so this difference can lead to data integrity issues. Synchronize the clocks on these servers.");
2050+
}
2051+
19992052
public abstract List<SQLFragment> getChangeStatements(TableChange change);
20002053

20012054
public abstract void purgeTempSchema(Map<String, TempTableTracker> createdTableNames);
@@ -2017,7 +2070,7 @@ protected void trackTempTables(Map<String, TempTableTracker> createdTableNames)
20172070
// Defragment an index, if necessary
20182071
public void defragmentIndex(DbSchema schema, String tableSelectName, String indexName)
20192072
{
2020-
// By default do nothing
2073+
// By default, do nothing
20212074
}
20222075

20232076
public boolean isTableExists(DbScope scope, String schema, String name)
@@ -2045,7 +2098,7 @@ public boolean hasTriggers(DbSchema scope, String schema, String tableName)
20452098

20462099
/**
20472100
*
2048-
* @return true If the dialect is one supported for the backend LabKey database. ie, Postgres or SQL Server
2101+
* @return true If the dialect is one supported for the backend LabKey database. i.e., Postgres or SQL Server
20492102
*/
20502103
public boolean isLabKeyDbDialect()
20512104
{
@@ -2419,5 +2472,37 @@ public void testProcedureIdentifierQuoting()
24192472
// A dotted name is rejected: schema and procedure are separate parameters, so a '.' in a single component is an ambiguous schema-qualified reference
24202473
assertThrows(IllegalArgumentException.class, () -> dialect.buildProcedureCall(core.getName(), "a.b", 0, false, false, scope));
24212474
}
2475+
2476+
// GH Issue #1223: Verify the server/database clock-skew arithmetic and warning threshold
2477+
@Test
2478+
public void testServerDatabaseTimeDifference()
2479+
{
2480+
LocalDateTime base = LocalDateTime.of(2026, 7, 6, 12, 0, 0);
2481+
2482+
// Identical times: zero difference, no warning
2483+
ServerDatabaseTimeDifference equal = new ServerDatabaseTimeDifference(base, base);
2484+
assertEquals(0, equal.getSeconds());
2485+
assertFalse(equal.exceedsWarningThreshold());
2486+
2487+
// Comfortably below the threshold: no warning
2488+
ServerDatabaseTimeDifference below = new ServerDatabaseTimeDifference(base, base.plusSeconds(5));
2489+
assertEquals(5, below.getSeconds());
2490+
assertFalse(below.exceedsWarningThreshold());
2491+
2492+
// Exactly at the threshold: no warning (comparison is strictly greater-than)
2493+
ServerDatabaseTimeDifference atThreshold = new ServerDatabaseTimeDifference(base, base.plusSeconds(TIME_DIFFERENCE_WARNING_SECONDS));
2494+
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS, atThreshold.getSeconds());
2495+
assertFalse(atThreshold.exceedsWarningThreshold());
2496+
2497+
// Just past the threshold: warning
2498+
ServerDatabaseTimeDifference over = new ServerDatabaseTimeDifference(base, base.plusSeconds(TIME_DIFFERENCE_WARNING_SECONDS + 1));
2499+
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS + 1, over.getSeconds());
2500+
assertTrue(over.exceedsWarningThreshold());
2501+
2502+
// Difference is absolute: database clock behind the web server is treated the same as ahead
2503+
ServerDatabaseTimeDifference behind = new ServerDatabaseTimeDifference(base, base.minusSeconds(TIME_DIFFERENCE_WARNING_SECONDS + 1));
2504+
assertEquals(TIME_DIFFERENCE_WARNING_SECONDS + 1, behind.getSeconds());
2505+
assertTrue(behind.exceedsWarningThreshold());
2506+
}
24222507
}
24232508
}

core/src/org/labkey/core/admin/admin.jsp

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<%@ page import="org.apache.commons.lang3.StringUtils" %>
2020
<%@ page import="org.labkey.api.admin.AdminBean" %>
2121
<%@ page import="org.labkey.api.data.DbScope" %>
22-
<%@ page import="org.labkey.api.data.SqlSelector" %>
22+
<%@ page import="org.labkey.api.data.dialect.SqlDialect" %>
2323
<%@ page import="org.labkey.api.files.FileContentService" %>
2424
<%@ page import="org.labkey.api.module.DefaultModule" %>
2525
<%@ page import="org.labkey.api.module.Module" %>
@@ -31,18 +31,18 @@
3131
<%@ page import="org.labkey.api.settings.AppProps" %>
3232
<%@ page import="org.labkey.api.util.Formats" %>
3333
<%@ page import="org.labkey.api.util.HtmlString"%>
34+
<%@ page import="org.labkey.api.util.HtmlStringBuilder" %>
3435
<%@ page import="org.labkey.api.view.NavTree" %>
3536
<%@ page import="org.labkey.core.admin.AdminController" %>
3637
<%@ page import="java.text.DecimalFormat" %>
37-
<%@ page import="java.time.Duration" %>
38-
<%@ page import="java.time.LocalDateTime" %>
3938
<%@ page import="java.time.format.DateTimeFormatter" %>
4039
<%@ page import="java.util.ArrayList" %>
4140
<%@ page import="java.util.Collection" %>
4241
<%@ page import="java.util.Comparator" %>
4342
<%@ page import="java.util.Map" %>
4443
<%@ page import="java.util.TreeMap" %>
4544
<%@ page import="org.apache.commons.lang3.Strings" %>
45+
<%@ page import="org.labkey.api.util.DateUtil" %>
4646
<%@ page extends="org.labkey.api.jsp.JspBase" %>
4747
<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %>
4848
<%
@@ -55,6 +55,7 @@
5555
.lk-admin-section { display: none; }
5656
.header-title { margin-bottom: 5px; }
5757
.header-link { margin: 0 5px 20px 0; }
58+
.lk-server-time-warning { color: red; }
5859
</style>
5960
<div class="row">
6061
<div class="col-sm-12 col-md-3">
@@ -91,16 +92,19 @@
9192
<br/>
9293
<%
9394
row = 0;
95+
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DateUtil.getJsonDateTimeFormatString());
96+
String timeCellCls = "";
97+
HtmlString warning = HtmlString.EMPTY_STRING;
9498
95-
LocalDateTime serverTime = LocalDateTime.now();
96-
LocalDateTime databaseTime = new SqlSelector(DbScope.getLabKeyScope(), "SELECT CURRENT_TIMESTAMP").getObject(LocalDateTime.class);
97-
long duration = Math.abs(Duration.between(serverTime, databaseTime).toSeconds());
98-
99-
// Warn if greater than this many seconds
100-
long warningSeconds = 10;
101-
102-
HtmlString style = unsafe(duration > warningSeconds ? " style=\"color:red;\"" : "");
103-
HtmlString warning = unsafe(duration > warningSeconds ? " - Warning: Web and database server times differ by " + duration + " seconds!" : "");
99+
SqlDialect.ServerDatabaseTimeDifference timeDifference = SqlDialect.getServerDatabaseTimeDifference(DbScope.getLabKeyScope());
100+
if (timeDifference.exceedsWarningThreshold())
101+
{
102+
timeCellCls = "lk-server-time-warning";
103+
warning = HtmlStringBuilder.of(" - Warning: Web and database server times differ by ")
104+
.append(timeDifference.getSeconds())
105+
.append(" seconds!")
106+
.getHtmlString();
107+
}
104108
%>
105109
<h4>Runtime Information</h4>
106110
<table class="labkey-data-region-legacy labkey-show-borders">
@@ -125,9 +129,9 @@
125129
<tr class="<%=getShadeRowClass(row++)%>"><td>Working Dir</td><td><%=h(AdminBean.workingDir)%></td></tr>
126130
<tr class="<%=getShadeRowClass(row++)%>"><td>Server GUID</td><td style="font-family:monospace"><%=h(AdminBean.serverGuid)%></td></tr>
127131
<tr class="<%=getShadeRowClass(row++)%>"><td>Server Session GUID</td><td style="font-family:monospace"><%=h(AdminBean.serverSessionGuid)%></td></tr>
128-
<tr class="<%=getShadeRowClass(row++)%>"><td>Server Startup Time</td><td<%=style%>><%=h(AdminBean.serverStartupTime)%></td></tr>
129-
<tr class="<%=getShadeRowClass(row++)%>"><td>Web Server Time</td><td<%=style%>><%=h(serverTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")))%><%=warning%></td></tr>
130-
<tr class="<%=getShadeRowClass(row++)%>"><td>Database Server Time</td><td<%=style%>><%=h(databaseTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")))%><%=warning%></td></tr>
132+
<tr class="<%=getShadeRowClass(row++)%>"><td>Server Startup Time</td><td><%=h(AdminBean.serverStartupTime)%></td></tr>
133+
<tr class="<%=getShadeRowClass(row++)%>"><td>Web Server Time</td><td class="<%=h(timeCellCls)%>"><%=h(dateTimeFormatter.format(timeDifference.serverTime()))%><%=warning%></td></tr>
134+
<tr class="<%=getShadeRowClass(row++)%>"><td>Database Server Time</td><td class="<%=h(timeCellCls)%>"><%=h(dateTimeFormatter.format(timeDifference.databaseTime()))%><%=warning%></td></tr>
131135
</table>
132136
</labkey:panel>
133137
<labkey:panel id="links" className="lk-admin-section">

core/src/org/labkey/core/dialect/PostgreSql92Dialect.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ public void addAdminWarningMessages(Warnings warnings, boolean showAllWarnings)
371371
super.addAdminWarningMessages(warnings, showAllWarnings);
372372
if (showAllWarnings)
373373
warnings.add(HtmlString.of(PostgreSqlDialectFactory.getStandardWarningMessage("has not been tested against", getMajorVersion() + ".x")));
374+
375+
addTimeDifferenceWarning(warnings, showAllWarnings);
374376
}
375377

376378
private int getIdentifierMaxByteLength()

experiment/src/org/labkey/experiment/api/ExpMaterialTableImpl.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@
139139
import java.io.IOException;
140140
import java.io.UncheckedIOException;
141141
import java.sql.Timestamp;
142-
import java.time.Duration;
143-
import java.time.LocalDateTime;
144142
import java.util.ArrayList;
145143
import java.util.Arrays;
146144
import java.util.Collection;
@@ -1406,19 +1404,15 @@ private _MaterializedQueryHelper getOrCreateMQH()
14061404
*/
14071405
private static boolean isIncrementalUpdateDisabled()
14081406
{
1407+
// Disable if web server and database time differ
14091408
if (_incrementalUpdateDisabled == null)
14101409
{
1411-
// borrowed from core/admin.jsp
1412-
LocalDateTime databaseTime = new SqlSelector(DbScope.getLabKeyScope(), "SELECT CURRENT_TIMESTAMP").getObject(LocalDateTime.class);
1413-
LocalDateTime serverTime = LocalDateTime.now();
1414-
1415-
// Disable if greater than this many seconds
1416-
long thresholdSeconds = 10;
1417-
long deltaSeconds = Math.abs(Duration.between(serverTime, databaseTime).toSeconds());
1418-
_incrementalUpdateDisabled = deltaSeconds > thresholdSeconds;
1410+
DbScope scope = DbScope.getLabKeyScope();
1411+
SqlDialect.ServerDatabaseTimeDifference difference = SqlDialect.getServerDatabaseTimeDifference(scope);
1412+
_incrementalUpdateDisabled = difference.exceedsWarningThreshold();
14191413

14201414
if (_incrementalUpdateDisabled)
1421-
_log.warn("Incremental update disabled for samples. Web and database server time differ by {} seconds which exceeds the threshold of {} seconds. You may experience degraded sample query performance.", deltaSeconds, thresholdSeconds);
1415+
_log.warn("Incremental update disabled for samples. Web and database server time differ by {} seconds which exceeds the threshold of {} seconds. You may experience degraded sample query performance.", difference.getSeconds(), SqlDialect.TIME_DIFFERENCE_WARNING_SECONDS);
14221416
}
14231417

14241418
return _incrementalUpdateDisabled;

0 commit comments

Comments
 (0)