5959import org .labkey .api .module .ModuleLoader ;
6060import org .labkey .api .query .FieldKey ;
6161import org .labkey .api .util .ExceptionUtil ;
62+ import org .labkey .api .util .HtmlString ;
6263import org .labkey .api .util .MemTracker ;
6364import org .labkey .api .util .StringUtilsLabKey ;
6465import org .labkey .api .util .SystemMaintenance ;
8081import java .sql .SQLException ;
8182import java .sql .Statement ;
8283import java .sql .Types ;
84+ import java .time .Duration ;
85+ import java .time .LocalDateTime ;
8386import java .util .ArrayList ;
8487import java .util .Arrays ;
8588import java .util .Calendar ;
9598import java .util .regex .Pattern ;
9699
97100import static org .junit .Assert .assertEquals ;
101+ import static org .junit .Assert .assertFalse ;
98102import static org .junit .Assert .assertThrows ;
99103import 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}
0 commit comments