Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persistence restore lastState and lastStateChange on startup #4463

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.persistence;

import java.time.Instant;
import java.time.ZonedDateTime;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.types.State;

/**
* This interface is used by persistence services to represent the full persisted state of an item, including the
* previous state, and last update and change timestamps.
* It can be used in restoring the full state of an item.
*
* @author Mark Herwege - Initial contribution
*/
@NonNullByDefault
public interface PersistedItem extends HistoricItem {

/**
* returns the timestamp of the last state change of the persisted item
*
* @return the timestamp of the last state change of the item
*/
@Nullable
ZonedDateTime getLastStateChange();

/**
* returns the timestamp of the last state change of the persisted item
*
* @return the timestamp of the last state change of the item
*/
@Nullable
default Instant getLastStateChangeInstant() {
ZonedDateTime lastStateChange = getLastStateChange();
return lastStateChange != null ? lastStateChange.toInstant() : null;
}

/**
* returns the last state of the item
*
* @return the last state
*/
@Nullable
State getLastState();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@
*/
package org.openhab.core.persistence;

import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.persistence.FilterCriteria.Ordering;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;

/**
* A queryable persistence service which can be used to store and retrieve
* data from openHAB. This is most likely some kind of database system.
*
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Added getItems method
* @author Mark Herwege - Added methods to retrieve lastUpdate, lastChange and lastState from persistence
*/
@NonNullByDefault
public interface QueryablePersistenceService extends PersistenceService {
Expand All @@ -43,4 +50,90 @@ public interface QueryablePersistenceService extends PersistenceService {
* @return a set of information about the persisted items
*/
Set<PersistenceItemInfo> getItemInfo();

/**
* 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.
*
* @return a {@link PersistedItem} or null if the item has not been persisted
*/
default @Nullable PersistedItem persistedItem(String itemName) {
State currentState = UnDefType.NULL;
State previousState = null;
ZonedDateTime lastUpdate = null;
ZonedDateTime lastChange = null;

int pageNumber = 0;
FilterCriteria filter = new FilterCriteria().setItemName(itemName).setEndDate(ZonedDateTime.now())
.setOrdering(Ordering.DESCENDING).setPageSize(1000).setPageNumber(pageNumber);
Iterable<HistoricItem> items = query(filter);
while (items != null) {
Iterator<HistoricItem> 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;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
* @author Mark Herwege - lastChange and nextChange methods
* @author Mark Herwege - handle persisted GroupItem with QuantityType
* @author Mark Herwege - add median methods
* @author Mark Herwege - use item lastChange and lastUpdate methods if not in peristence
*/
@Component(immediate = true)
@NonNullByDefault
Expand Down Expand Up @@ -331,6 +332,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable

/**
* Query the last historic update time of a given <code>item</code>. The default persistence service is used.
* Note the {@link Item.getLastStateUpdate()} is generally preferred to get the last update time of an item.
*
* @param item the item for which the last historic update time is to be returned
* @return point in time of the last historic update to <code>item</code>, <code>null</code> if there are no
Expand All @@ -343,6 +345,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable

/**
* Query for the last historic update time of a given <code>item</code>.
* Note the {@link Item.getLastStateUpdate()} is generally preferred to get the last update time of an item.
*
* @param item the item for which the last historic update time is to be returned
* @param serviceId the name of the {@link PersistenceService} to use
Expand Down Expand Up @@ -386,6 +389,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable

/**
* Query the last historic change time of a given <code>item</code>. The default persistence service is used.
* Note the {@link Item.getLastStateChange()} is generally preferred to get the last state change time of an item.
*
* @param item the item for which the last historic change time is to be returned
* @return point in time of the last historic change to <code>item</code>, <code>null</code> if there are no
Expand All @@ -398,6 +402,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable

/**
* Query for the last historic change time of a given <code>item</code>.
* Note the {@link Item.getLastStateChange()} is generally preferred to get the last state change time of an item.
*
* @param item the item for which the last historic change time is to be returned
* @param serviceId the name of the {@link PersistenceService} to use
Expand Down Expand Up @@ -461,26 +466,26 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable
filter.setPageNumber(startPage);

Iterable<HistoricItem> items = qService.query(filter);
Iterator<HistoricItem> itemIterator = items.iterator();
State state = item.getState();
if (itemIterator.hasNext()) {
if (!skipEqual) {
HistoricItem historicItem = itemIterator.next();
if (!forward && !historicItem.getState().equals(state)) {
// Last persisted state value different from current state value, so it must have updated since
// last persist. We do not know when.
return null;
}
return historicItem.getTimestamp();
} else {
HistoricItem historicItem = itemIterator.next();
int itemCount = 1;
if (!historicItem.getState().equals(state)) {
// Persisted state value different from current state value, so it must have changed, but we do
// not know when when looking backward.
return forward ? historicItem.getTimestamp() : null;
}
while (items != null) {
while (items != null) {
Iterator<HistoricItem> itemIterator = items.iterator();
int itemCount = 0;
State state = item.getState();
if (itemIterator.hasNext()) {
if (!skipEqual) {
HistoricItem historicItem = itemIterator.next();
if (!forward && !historicItem.getState().equals(state)) {
// Last persisted state value different from current state value, so it must have updated
// since last persist. We do not know when from persistence, so get it from the item.
return item.getLastStateUpdate();
}
return historicItem.getTimestamp();
} else {
HistoricItem historicItem = itemIterator.next();
if (!historicItem.getState().equals(state)) {
// Persisted state value different from current state value, so it must have changed, but we
// do not know when looking backward in persistence. Get it from the item.
return forward ? historicItem.getTimestamp() : item.getLastStateChange();
}
while (historicItem.getState().equals(state) && itemIterator.hasNext()) {
HistoricItem nextHistoricItem = itemIterator.next();
itemCount++;
Expand All @@ -492,7 +497,6 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable
if (itemCount == filter.getPageSize()) {
filter.setPageNumber(++startPage);
items = qService.query(filter);
itemCount = 0;
} else {
items = null;
}
Expand All @@ -508,6 +512,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable

/**
* Returns the previous state of a given <code>item</code>.
* Note the {@link Item.getLastState()} is generally preferred to get the previous state of an item.
*
* @param item the item to get the previous state value for
* @return the previous state or <code>null</code> if no previous state could be found, or if the default
Expand All @@ -519,6 +524,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable

/**
* Returns the previous state of a given <code>item</code>.
* Note the {@link Item.getLastState()} is generally preferred to get the previous state of an item.
*
* @param item the item to get the previous state value for
* @param skipEqual if true, skips equal state values and searches the first state not equal the current state
Expand All @@ -532,6 +538,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable
/**
* Returns the previous state of a given <code>item</code>.
* The {@link PersistenceService} identified by the <code>serviceId</code> is used.
* Note the {@link Item.getLastState()} is generally preferred to get the previous state of an item.
*
* @param item the item to get the previous state value for
* @param serviceId the name of the {@link PersistenceService} to use
Expand All @@ -545,6 +552,7 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable
/**
* Returns the previous state of a given <code>item</code>.
* The {@link PersistenceService} identified by the <code>serviceId</code> is used.
* Note the {@link Item.getLastState()} is generally preferred to get the previous state of an item.
*
* @param item the item to get the previous state value for
* @param skipEqual if <code>true</code>, skips equal state values and searches the first state not equal the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
package org.openhab.core.persistence.internal;

import static org.openhab.core.persistence.FilterCriteria.Ordering.ASCENDING;
import static org.openhab.core.persistence.strategy.PersistenceStrategy.Globals.FORECAST;
import static org.openhab.core.persistence.strategy.PersistenceStrategy.Globals.RESTORE;
import static org.openhab.core.persistence.strategy.PersistenceStrategy.Globals.UPDATE;
import static org.openhab.core.persistence.strategy.PersistenceStrategy.Globals.*;

import java.time.Instant;
import java.time.ZoneId;
Expand Down Expand Up @@ -50,6 +48,7 @@
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.ModifiablePersistenceService;
import org.openhab.core.persistence.PersistedItem;
import org.openhab.core.persistence.PersistenceItemConfiguration;
import org.openhab.core.persistence.PersistenceManager;
import org.openhab.core.persistence.PersistenceService;
Expand Down Expand Up @@ -90,6 +89,7 @@
* @author Markus Rathgeb - Separation of persistence core and model, drop Quartz usage.
* @author Jan N. Klug - Refactored to use service configuration registry
* @author Jan N. Klug - Added time series support
* @author Mark Herwege - Added restoring lastState, lastStateChange and lastStateUpdate
*/
@Component(immediate = true, service = PersistenceManager.class)
@NonNullByDefault
Expand Down Expand Up @@ -521,36 +521,30 @@ public void removeItem(String itemName) {
private void restoreItemStateIfPossible(Item item) {
QueryablePersistenceService queryService = (QueryablePersistenceService) persistenceService;

FilterCriteria filter = new FilterCriteria().setItemName(item.getName()).setEndDate(ZonedDateTime.now())
.setPageSize(1);
Iterable<HistoricItem> result = safeCaller.create(queryService, QueryablePersistenceService.class)
PersistedItem persistedItem = safeCaller.create(queryService, QueryablePersistenceService.class)
.onTimeout(
() -> logger.warn("Querying persistence service '{}' to restore '{}' takes more than {}ms.",
queryService.getId(), item.getName(), SafeCaller.DEFAULT_TIMEOUT))
.onException(e -> logger.error(
"Exception occurred while querying persistence service '{}' to restore '{}': {}",
queryService.getId(), item.getName(), e.getMessage(), e))
.build().query(filter);
if (result == null) {
// in case of an exception or timeout, the safe caller returns null
.build().persistedItem(item.getName());
if (persistedItem == null) {
return;
}
Iterator<HistoricItem> it = result.iterator();
if (it.hasNext()) {
HistoricItem historicItem = it.next();
GenericItem genericItem = (GenericItem) item;
if (!UnDefType.NULL.equals(item.getState())) {
// someone else already restored the state or a new state was set
return;
}
genericItem.removeStateChangeListener(PersistenceManagerImpl.this);
genericItem.setState(historicItem.getState());
genericItem.addStateChangeListener(PersistenceManagerImpl.this);
if (logger.isDebugEnabled()) {
logger.debug("Restored item state from '{}' for item '{}' -> '{}'",
DateTimeFormatter.ISO_ZONED_DATE_TIME.format(historicItem.getTimestamp()), item.getName(),
historicItem.getState());
}
GenericItem genericItem = (GenericItem) item;
if (!UnDefType.NULL.equals(item.getState())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not done before querying the persistence service?
It seems that we could've avoided running the query unnecessarily if the state was already initialised.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, as the query runs in a separate thread, the item state may have changed in between starting the query and returning the result.
@J-N-K introduced this. I didn't find more explanation for it, but this was my interpretation. So I think it should remain there.

Copy link
Contributor

@jimtng jimtng Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, as the query runs in a separate thread, the item state may have changed in between starting the query and returning the result.

That makes sense, but I think it would also make sense to check it beforehand as well.

But perhaps if that were the case, such change could be added later, and separately from this PR.

// someone else already restored the state or a new state was set
return;
}
genericItem.removeStateChangeListener(PersistenceManagerImpl.this);
genericItem.setState(persistedItem.getState(), persistedItem.getLastState(), persistedItem.getTimestamp(),
persistedItem.getLastStateChange());
genericItem.addStateChangeListener(PersistenceManagerImpl.this);
if (logger.isDebugEnabled()) {
logger.debug("Restored item state from '{}' for item '{}' -> '{}'",
DateTimeFormatter.ISO_ZONED_DATE_TIME.format(persistedItem.getTimestamp()), item.getName(),
persistedItem.getState());
}
}

Expand Down
Loading