Skip to content

Commit

Permalink
[inmemory] Initial contribution (openhab#15063)
Browse files Browse the repository at this point in the history
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
J-N-K authored and matchews committed Aug 9, 2023
1 parent b767486 commit 06fee61
Show file tree
Hide file tree
Showing 9 changed files with 575 additions and 0 deletions.
5 changes: 5 additions & 0 deletions bom/openhab-addons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,11 @@
<artifactId>org.openhab.persistence.influxdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.inmemory</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.persistence.jdbc</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions bundles/org.openhab.persistence.inmemory/NOTICE
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
14 changes: 14 additions & 0 deletions bundles/org.openhab.persistence.inmemory/README.md
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.
17 changes: 17 additions & 0 deletions bundles/org.openhab.persistence.inmemory/pom.xml
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>
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>
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) {
};
}
Loading

0 comments on commit 06fee61

Please sign in to comment.