forked from openhab/openhab-addons
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[inmemory] Initial contribution (openhab#15063)
This is the initial contribution of a new volatile persistence service. It does store values in memory only and can especially be used for forecasts or other data where volatile storage is sufficient. Signed-off-by: Jan N. Klug <[email protected]> Signed-off-by: Matt Myers <[email protected]>
- Loading branch information
Showing
9 changed files
with
575 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
This content is produced and maintained by the openHAB project. | ||
|
||
* Project home: https://www.openhab.org | ||
|
||
== Declared Project Licenses | ||
|
||
This program and the accompanying materials are made available under the terms | ||
of the Eclipse Public License 2.0 which is available at | ||
https://www.eclipse.org/legal/epl-2.0/. | ||
|
||
== Source Code | ||
|
||
https://github.com/openhab/openhab-addons |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# InMemory Persistence | ||
|
||
The InMemory persistence service provides a volatile storage, i.e. it is cleared on shutdown. | ||
Because of that the `restoreOnStartup` strategy is not supported for this service. | ||
|
||
The main use-case is to store data that is needed during runtime, e.g. temporary storage of forecast data that is retrieved from a binding. | ||
|
||
Since all data is stored in memory only, there is no default strategy for this service. | ||
Unlike other persistence services, you MUST add a configuration, otherwise no data will be persisted. | ||
To avoid excessive memory usage, it is recommended to persist only a limited number of items and use a strategy that stores only data that is actually needed. | ||
|
||
The service has a global configuration option `maxEntries` to limit the number of datapoints per item, the default value is `512`. | ||
When the number of datapoints is reached and a new value is persisted, the oldest (by timestamp) value will be removed. | ||
A `maxEntries` value of `0` disables automatic purging. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
|
||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>org.openhab.addons.bundles</groupId> | ||
<artifactId>org.openhab.addons.reactor.bundles</artifactId> | ||
<version>4.0.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>org.openhab.persistence.inmemory</artifactId> | ||
|
||
<name>openHAB Add-ons :: Bundles :: Persistence Service :: InMemory</name> | ||
|
||
</project> |
10 changes: 10 additions & 0 deletions
10
bundles/org.openhab.persistence.inmemory/src/main/feature/feature.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<features name="org.openhab.persistence.inmemory-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0"> | ||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository> | ||
|
||
<feature name="openhab-persistence-inmemory" description="InMemory Persistence" version="${project.version}"> | ||
<feature>openhab-runtime-base</feature> | ||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.persistence.inmemory/${project.version}</bundle> | ||
</feature> | ||
|
||
</features> |
311 changes: 311 additions & 0 deletions
311
...y/src/main/java/org/openhab/persistence/inmemory/internal/InMemoryPersistenceService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,311 @@ | ||
/** | ||
* Copyright (c) 2010-2023 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.persistence.inmemory.internal; | ||
|
||
import java.time.Instant; | ||
import java.time.ZonedDateTime; | ||
import java.util.Comparator; | ||
import java.util.Date; | ||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import java.util.TreeSet; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.locks.Lock; | ||
import java.util.concurrent.locks.ReentrantLock; | ||
import java.util.stream.Collectors; | ||
|
||
import org.eclipse.jdt.annotation.NonNullByDefault; | ||
import org.eclipse.jdt.annotation.Nullable; | ||
import org.openhab.core.config.core.ConfigParser; | ||
import org.openhab.core.config.core.ConfigurableService; | ||
import org.openhab.core.items.Item; | ||
import org.openhab.core.persistence.FilterCriteria; | ||
import org.openhab.core.persistence.HistoricItem; | ||
import org.openhab.core.persistence.ModifiablePersistenceService; | ||
import org.openhab.core.persistence.PersistenceItemInfo; | ||
import org.openhab.core.persistence.PersistenceService; | ||
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; | ||
import org.osgi.service.component.annotations.Deactivate; | ||
import org.osgi.service.component.annotations.Modified; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* This is the implementation of the volatile {@link PersistenceService}. | ||
* | ||
* @author Jan N. Klug - Initial contribution | ||
*/ | ||
@NonNullByDefault | ||
@Component(service = { PersistenceService.class, | ||
ModifiablePersistenceService.class }, configurationPid = "org.openhab.inmemory", // | ||
property = Constants.SERVICE_PID + "=org.openhab.inmemory") | ||
@ConfigurableService(category = "persistence", label = "InMemory Persistence Service", description_uri = InMemoryPersistenceService.CONFIG_URI) | ||
public class InMemoryPersistenceService implements ModifiablePersistenceService { | ||
|
||
private static final String SERVICE_ID = "inmemory"; | ||
private static final String SERVICE_LABEL = "In Memory"; | ||
|
||
protected static final String CONFIG_URI = "persistence:inmemory"; | ||
private final String MAX_ENTRIES_CONFIG = "maxEntries"; | ||
private final long MAX_ENTRIES_DEFAULT = 512; | ||
|
||
private final Logger logger = LoggerFactory.getLogger(InMemoryPersistenceService.class); | ||
|
||
private final Map<String, PersistItem> persistMap = new ConcurrentHashMap<>(); | ||
private long maxEntries = MAX_ENTRIES_DEFAULT; | ||
|
||
@Activate | ||
public void activate(Map<String, Object> config) { | ||
modified(config); | ||
logger.debug("InMemory persistence service is now activated."); | ||
} | ||
|
||
@Modified | ||
public void modified(Map<String, Object> config) { | ||
maxEntries = ConfigParser.valueAsOrElse(config.get(MAX_ENTRIES_CONFIG), Long.class, MAX_ENTRIES_DEFAULT); | ||
|
||
persistMap.values().forEach(persistItem -> { | ||
Lock lock = persistItem.lock(); | ||
lock.lock(); | ||
try { | ||
while (persistItem.database().size() > maxEntries) { | ||
persistItem.database().pollFirst(); | ||
} | ||
} finally { | ||
lock.unlock(); | ||
} | ||
}); | ||
} | ||
|
||
@Deactivate | ||
public void deactivate() { | ||
logger.debug("InMemory persistence service deactivated."); | ||
} | ||
|
||
@Override | ||
public String getId() { | ||
return SERVICE_ID; | ||
} | ||
|
||
@Override | ||
public String getLabel(@Nullable Locale locale) { | ||
return SERVICE_LABEL; | ||
} | ||
|
||
@Override | ||
public Set<PersistenceItemInfo> getItemInfo() { | ||
return persistMap.entrySet().stream().map(this::toItemInfo).collect(Collectors.toSet()); | ||
} | ||
|
||
@Override | ||
public void store(Item item) { | ||
internalStore(item.getName(), ZonedDateTime.now(), item.getState()); | ||
} | ||
|
||
@Override | ||
public void store(Item item, @Nullable String alias) { | ||
String finalName = Objects.requireNonNullElse(alias, item.getName()); | ||
internalStore(finalName, ZonedDateTime.now(), item.getState()); | ||
} | ||
|
||
@Override | ||
public void store(Item item, ZonedDateTime date, State state) { | ||
internalStore(item.getName(), date, state); | ||
} | ||
|
||
@Override | ||
public boolean remove(FilterCriteria filter) throws IllegalArgumentException { | ||
String itemName = filter.getItemName(); | ||
if (itemName == null) { | ||
return false; | ||
} | ||
|
||
PersistItem persistItem = persistMap.get(itemName); | ||
if (persistItem == null) { | ||
return false; | ||
} | ||
|
||
Lock lock = persistItem.lock(); | ||
lock.lock(); | ||
try { | ||
List<PersistEntry> toRemove = persistItem.database().stream().filter(e -> applies(e, filter)).toList(); | ||
toRemove.forEach(persistItem.database()::remove); | ||
} finally { | ||
lock.unlock(); | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
public Iterable<HistoricItem> query(FilterCriteria filter) { | ||
String itemName = filter.getItemName(); | ||
if (itemName == null) { | ||
return List.of(); | ||
} | ||
|
||
PersistItem persistItem = persistMap.get(itemName); | ||
if (persistItem == null) { | ||
return List.of(); | ||
} | ||
|
||
Lock lock = persistItem.lock(); | ||
lock.lock(); | ||
try { | ||
return persistItem.database().stream().filter(e -> applies(e, filter)).map(e -> toHistoricItem(itemName, e)) | ||
.toList(); | ||
} finally { | ||
lock.unlock(); | ||
} | ||
} | ||
|
||
@Override | ||
public List<PersistenceStrategy> getDefaultStrategies() { | ||
// persist nothing by default | ||
return List.of(); | ||
} | ||
|
||
private PersistenceItemInfo toItemInfo(Map.Entry<String, PersistItem> itemEntry) { | ||
Lock lock = itemEntry.getValue().lock(); | ||
lock.lock(); | ||
try { | ||
String name = itemEntry.getKey(); | ||
Integer count = itemEntry.getValue().database().size(); | ||
Instant earliest = itemEntry.getValue().database().first().timestamp().toInstant(); | ||
Instant latest = itemEntry.getValue().database.last().timestamp.toInstant(); | ||
return new PersistenceItemInfo() { | ||
|
||
@Override | ||
public String getName() { | ||
return name; | ||
} | ||
|
||
@Override | ||
public @Nullable Integer getCount() { | ||
return count; | ||
} | ||
|
||
@Override | ||
public @Nullable Date getEarliest() { | ||
return Date.from(earliest); | ||
} | ||
|
||
@Override | ||
public @Nullable Date getLatest() { | ||
return Date.from(latest); | ||
} | ||
}; | ||
} finally { | ||
lock.unlock(); | ||
} | ||
} | ||
|
||
private HistoricItem toHistoricItem(String itemName, PersistEntry entry) { | ||
return new HistoricItem() { | ||
@Override | ||
public ZonedDateTime getTimestamp() { | ||
return entry.timestamp(); | ||
} | ||
|
||
@Override | ||
public State getState() { | ||
return entry.state(); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return itemName; | ||
} | ||
}; | ||
} | ||
|
||
private void internalStore(String itemName, ZonedDateTime timestamp, State state) { | ||
if (state instanceof UnDefType) { | ||
return; | ||
} | ||
|
||
PersistItem persistItem = Objects.requireNonNull(persistMap.computeIfAbsent(itemName, | ||
k -> new PersistItem(new TreeSet<>(Comparator.comparing(PersistEntry::timestamp)), | ||
new ReentrantLock()))); | ||
|
||
Lock lock = persistItem.lock(); | ||
lock.lock(); | ||
try { | ||
persistItem.database().add(new PersistEntry(timestamp, state)); | ||
|
||
while (persistItem.database.size() > maxEntries) { | ||
persistItem.database().pollFirst(); | ||
} | ||
} finally { | ||
lock.unlock(); | ||
} | ||
} | ||
|
||
@SuppressWarnings({ "rawType", "unchecked" }) | ||
private boolean applies(PersistEntry entry, FilterCriteria filter) { | ||
ZonedDateTime beginDate = filter.getBeginDate(); | ||
if (beginDate != null && entry.timestamp().isBefore(beginDate)) { | ||
return false; | ||
} | ||
ZonedDateTime endDate = filter.getEndDate(); | ||
if (endDate != null && entry.timestamp().isAfter(endDate)) { | ||
return false; | ||
} | ||
|
||
State refState = filter.getState(); | ||
FilterCriteria.Operator operator = filter.getOperator(); | ||
if (refState == null) { | ||
// no state filter | ||
return true; | ||
} | ||
|
||
if (operator == FilterCriteria.Operator.EQ) { | ||
return entry.state().equals(refState); | ||
} | ||
|
||
if (operator == FilterCriteria.Operator.NEQ) { | ||
return !entry.state().equals(refState); | ||
} | ||
|
||
if (entry.state() instanceof Comparable comparableState && entry.state.getClass().equals(refState.getClass())) { | ||
if (operator == FilterCriteria.Operator.GT) { | ||
return comparableState.compareTo(refState) > 0; | ||
} | ||
if (operator == FilterCriteria.Operator.GTE) { | ||
return comparableState.compareTo(refState) >= 0; | ||
} | ||
if (operator == FilterCriteria.Operator.LT) { | ||
return comparableState.compareTo(refState) < 0; | ||
} | ||
if (operator == FilterCriteria.Operator.LTE) { | ||
return comparableState.compareTo(refState) <= 0; | ||
} | ||
} else { | ||
logger.warn("Using operator {} but state {} is not comparable!", operator, refState); | ||
} | ||
return true; | ||
} | ||
|
||
private record PersistEntry(ZonedDateTime timestamp, State state) { | ||
}; | ||
|
||
private record PersistItem(TreeSet<PersistEntry> database, Lock lock) { | ||
}; | ||
} |
Oops, something went wrong.