From 1cefae42fdcc3b190b1ddd5b47471a8c93c21b29 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Sat, 22 Feb 2025 11:21:51 +0100 Subject: [PATCH] fix rrd4j restore on startup (#18308) Signed-off-by: Mark Herwege --- .../internal/RRD4jPersistenceService.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/bundles/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jPersistenceService.java b/bundles/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jPersistenceService.java index 1c286d42fae..068e2c9a1eb 100644 --- a/bundles/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jPersistenceService.java +++ b/bundles/org.openhab.persistence.rrd4j/src/main/java/org/openhab/persistence/rrd4j/internal/RRD4jPersistenceService.java @@ -64,12 +64,14 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.FilterCriteria.Ordering; import org.openhab.core.persistence.HistoricItem; +import org.openhab.core.persistence.PersistedItem; import org.openhab.core.persistence.PersistenceItemInfo; import org.openhab.core.persistence.PersistenceService; import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.strategy.PersistenceCronStrategy; import org.openhab.core.persistence.strategy.PersistenceStrategy; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -539,6 +541,118 @@ public class RRD4jPersistenceService implements QueryablePersistenceService { } } + /** + * Returns a {@link PersistedItem} representing the persisted state, last update and change timestamps and previous + * persisted state. This can be used to restore the full state of an item. + * The default implementation queries the service and iterates backward to find the last change and previous + * persisted state. Persistence services can override this default implementation with a more specific or efficient + * algorithm. + * + * This method overrides the default implementation in the interface as queries without a begin date are not allowed + * in the rrd4j database. If the last change cannot be found in half the length of the first archive, a null value + * for the last change and previous persisted state will be returned with {@link PersistedItem}. + * + * @param itemName name of item + * @param alias alias of item + * + * @return a {@link PersistedItem} or null if the item has not been persisted + */ + @Override + public @Nullable PersistedItem persistedItem(String itemName, @Nullable String alias) { + State currentState = UnDefType.NULL; + State previousState = null; + ZonedDateTime lastUpdate = null; + ZonedDateTime lastChange = null; + + // Avoid query with open begin date. Don't look further back than half of the first archive. + // Only half of the archive is considered to avoid accidently querying the next archive with lower granularity. + String localAlias = alias != null ? alias : itemName; + RrdDefConfig rrdDefConfig = getRrdDefConfig(localAlias); + if (rrdDefConfig == null) { + logger.warn("No rrd4j database definition found for {}", itemName); + return null; + } + List rrdArchiveDefs = rrdDefConfig.archives; + if (rrdArchiveDefs.isEmpty()) { + logger.warn("No rrd4j archive definition found for {}", itemName); + return null; + } + RrdArchiveDef rrdArchiveDef = rrdArchiveDefs.get(0); + long archiveLength = (rrdArchiveDef.rows * rrdArchiveDef.steps) / 2; + ZonedDateTime endDate = ZonedDateTime.now(); + ZonedDateTime beginDate = endDate.minusSeconds(archiveLength); + + int pageNumber = 0; + FilterCriteria filter = new FilterCriteria().setItemName(itemName).setBeginDate(beginDate).setEndDate(endDate) + .setOrdering(Ordering.DESCENDING).setPageSize(1000).setPageNumber(pageNumber); + Iterable items = query(filter, alias); + while (items != null) { + Iterator it = items.iterator(); + int itemCount = 0; + if (UnDefType.NULL.equals(currentState) && it.hasNext()) { + HistoricItem historicItem = it.next(); + itemCount++; + currentState = historicItem.getState(); + lastUpdate = historicItem.getTimestamp(); + lastChange = lastUpdate; + } + while (it.hasNext()) { + HistoricItem historicItem = it.next(); + itemCount++; + if (!historicItem.getState().equals(currentState)) { + previousState = historicItem.getState(); + items = null; + break; + } + lastChange = historicItem.getTimestamp(); + } + if (itemCount == filter.getPageSize()) { + filter.setPageNumber(++pageNumber); + items = query(filter); + } else { + items = null; + } + } + + if (UnDefType.NULL.equals(currentState) || lastUpdate == null) { + return null; + } + + final State state = currentState; + final ZonedDateTime lastStateUpdate = lastUpdate; + final State lastState = previousState; + // if we don't find a previous state in persistence, we also don't know when it last changed + final ZonedDateTime lastStateChange = previousState != null ? lastChange : null; + + return new PersistedItem() { + + @Override + public ZonedDateTime getTimestamp() { + return lastStateUpdate; + } + + @Override + public State getState() { + return state; + } + + @Override + public String getName() { + return itemName; + } + + @Override + public @Nullable ZonedDateTime getLastStateChange() { + return lastStateChange; + } + + @Override + public @Nullable State getLastState() { + return lastState; + } + }; + } + @Override public Set getItemInfo() { return Set.of();