From 5da21becf1ea74d38de9e1857bc67e9325c7c965 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Wed, 7 Oct 2026 15:56:32 +0200 Subject: [PATCH] update powerstate timestamps to the right timezone --- .../com/cloud/vm/dao/VMInstanceDaoImpl.java | 4 +- .../cloud/vm/dao/VMInstanceDaoImplTest.java | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 518bc3cf497c..232a99211a10 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -19,6 +19,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; import java.util.Date; @@ -1037,7 +1038,7 @@ public Map updatePowerState( return notUpdated; } StringBuilder sql = new StringBuilder("UPDATE `cloud`.`vm_instance` SET " + - "`power_host` = ?, `power_state_update_time` = now(), `power_state` = CASE "); + "`power_host` = ?, `power_state_update_time` = ?, `power_state` = CASE "); updateCounts.keySet().forEach(key -> { sql.append("WHEN id = ").append(key).append(" THEN '").append(instancePowerStates.get(key)).append("' "); }); @@ -1052,6 +1053,7 @@ public Map updatePowerState( TransactionLegacy txn = TransactionLegacy.currentTxn(); try (PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql.toString())) { pstmt.setLong(1, powerHostId); + pstmt.setTimestamp(2, new Timestamp(DateUtil.currentGMTTime().getTime()), gmtCalendar()); pstmt.executeUpdate(); } catch (SQLException e) { logger.error("Unable to execute update power states SQL from VMs {} due to: {}", diff --git a/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java b/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java index 5f8b2dd90eca..5317f3a66897 100644 --- a/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java +++ b/engine/schema/src/test/java/com/cloud/vm/dao/VMInstanceDaoImplTest.java @@ -20,19 +20,26 @@ import static com.cloud.vm.VirtualMachine.State.Running; import static com.cloud.vm.VirtualMachine.State.Stopped; import static com.cloud.vm.dao.VMInstanceDaoImpl.MAX_CONSECUTIVE_SAME_STATE_UPDATE_COUNT; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.sql.PreparedStatement; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; import com.cloud.host.dao.HostDao; import org.joda.time.DateTime; @@ -40,8 +47,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @@ -50,6 +59,7 @@ import com.cloud.utils.db.Filter; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.TransactionLegacy; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import org.mockito.junit.MockitoJUnitRunner; @@ -210,6 +220,51 @@ public void testUpdatePowerStateNoChangeMaxUpdatesInvalidStateVmRunning() { assertTrue(result); } + @Test + public void testUpdatePowerStatesBindsGmtUpdateTimeInsteadOfDbNow() throws Exception { + TimeZone defaultTimeZone = TimeZone.getDefault(); + try { + for (String timeZone : List.of("UTC", "Europe/Amsterdam", "America/New_York", "Asia/Kolkata", "Pacific/Auckland")) { + TimeZone.setDefault(TimeZone.getTimeZone(timeZone)); + verifyBulkUpdatePowerStatesBindsGmtUpdateTime(timeZone); + } + } finally { + TimeZone.setDefault(defaultTimeZone); + } + } + + private void verifyBulkUpdatePowerStatesBindsGmtUpdateTime(String timeZone) throws Exception { + VMInstanceVO instance = Mockito.mock(VMInstanceVO.class); + when(instance.getId()).thenReturn(1L); + when(instance.getPowerState()).thenReturn(VirtualMachine.PowerState.PowerOn); + doReturn(List.of(instance)).when(vmInstanceDao).listSelectPowerStateByIds(any()); + TransactionLegacy txn = Mockito.mock(TransactionLegacy.class); + PreparedStatement pstmt = Mockito.mock(PreparedStatement.class); + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + when(txn.prepareAutoCloseStatement(sqlCaptor.capture())).thenReturn(pstmt); + ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(Timestamp.class); + ArgumentCaptor calendarCaptor = ArgumentCaptor.forClass(Calendar.class); + + long before = System.currentTimeMillis(); + Map notUpdated; + try (MockedStatic ignored = Mockito.mockStatic(TransactionLegacy.class)) { + ignored.when(TransactionLegacy::currentTxn).thenReturn(txn); + notUpdated = vmInstanceDao.updatePowerState(Map.of(1L, VirtualMachine.PowerState.PowerOff), 2L, new Date()); + } + long after = System.currentTimeMillis(); + + assertTrue(timeZone, notUpdated.isEmpty()); + String sql = sqlCaptor.getValue(); + assertFalse(timeZone, sql.toLowerCase().contains("now()")); + assertTrue(timeZone, sql.contains("`power_state_update_time` = ?")); + verify(pstmt).setLong(1, 2L); + verify(pstmt).setTimestamp(eq(2), timeCaptor.capture(), calendarCaptor.capture()); + verify(pstmt).executeUpdate(); + long written = timeCaptor.getValue().getTime(); + assertTrue(timeZone, written >= before && written <= after); + assertEquals(timeZone, "GMT", calendarCaptor.getValue().getTimeZone().getID()); + } + @Test public void testSearchRemovedByRemoveDate() { SearchBuilder sb = Mockito.mock(SearchBuilder.class);