25 changed files with 492 additions and 104 deletions
@ -0,0 +1,91 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.cache; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.cache.Cache; |
||||
|
import org.springframework.cache.CacheManager; |
||||
|
import org.thingsboard.server.common.data.HasVersion; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
import java.util.concurrent.locks.Lock; |
||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||
|
|
||||
|
@RequiredArgsConstructor |
||||
|
public abstract class VersionedCaffeineTbTransactionalCache<K extends Serializable, V extends Serializable & HasVersion> implements VersionedTbTransactionalCache<K, V> { |
||||
|
|
||||
|
private final CacheManager cacheManager; |
||||
|
private final String cacheName; |
||||
|
|
||||
|
private final Lock lock = new ReentrantLock(); |
||||
|
|
||||
|
@Override |
||||
|
public TbCacheValueWrapper<V> get(K key) { |
||||
|
return SimpleTbCacheValueWrapper.wrap(doGet(key).getSecond()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void put(K key, V value) { |
||||
|
Long version = value != null ? value.getVersion() : 0; |
||||
|
put(key, value, version); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void put(K key, V value, Long version) { |
||||
|
if (version == null) { |
||||
|
return; |
||||
|
} |
||||
|
lock.lock(); |
||||
|
try { |
||||
|
TbPair<Long, V> versionValuePair = doGet(key); |
||||
|
Long currentVersion = versionValuePair.getFirst(); |
||||
|
if (currentVersion == null || version >= currentVersion) { |
||||
|
cacheManager.getCache(cacheName).put(key, TbPair.of(version, value)); |
||||
|
} |
||||
|
} finally { |
||||
|
lock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private TbPair<Long, V> doGet(K key) { |
||||
|
Cache.ValueWrapper source = cacheManager.getCache(cacheName).get(key); |
||||
|
return source == null ? TbPair.emptyPair() : (TbPair<Long, V>) source.get(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void evict(K key) { |
||||
|
lock.lock(); |
||||
|
try { |
||||
|
cacheManager.getCache(cacheName).evict(key); |
||||
|
} finally { |
||||
|
lock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void evict(K key, Long version) { |
||||
|
if (version == null) { |
||||
|
return; |
||||
|
} |
||||
|
lock.lock(); |
||||
|
try { |
||||
|
put(key, null, version); |
||||
|
} finally { |
||||
|
lock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,163 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.cache; |
||||
|
|
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.lang3.NotImplementedException; |
||||
|
import org.springframework.dao.InvalidDataAccessApiUsageException; |
||||
|
import org.springframework.data.redis.connection.RedisConnection; |
||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
|
import org.springframework.data.redis.connection.ReturnType; |
||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer; |
||||
|
import org.thingsboard.server.common.data.HasVersion; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.Collection; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class VersionedRedisTbTransactionalCache<K extends Serializable, V extends Serializable & HasVersion> extends RedisTbTransactionalCache<K, V> implements VersionedTbTransactionalCache<K, V> { |
||||
|
|
||||
|
private static final int VERSION_SIZE = 8; |
||||
|
private static final int VALUE_END_OFFSET = -1; |
||||
|
|
||||
|
static final byte[] SET_VERSIONED_VALUE_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize(""" |
||||
|
-- KEYS[1] is the key |
||||
|
-- ARGV[1] is the new value |
||||
|
-- ARGV[2] is the new version |
||||
|
|
||||
|
local key = KEYS[1] |
||||
|
local newValue = ARGV[1] |
||||
|
local newVersion = tonumber(ARGV[2]) |
||||
|
|
||||
|
-- Function to set the new value with the version |
||||
|
local function setNewValue() |
||||
|
local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue:sub(9) |
||||
|
redis.call('SET', key, newValueWithVersion) |
||||
|
end |
||||
|
|
||||
|
-- Get the current version (first 8 bytes) of the current value |
||||
|
local currentVersionBytes = redis.call('GETRANGE', key, 0, 7) |
||||
|
|
||||
|
if currentVersionBytes and #currentVersionBytes == 8 then |
||||
|
-- Extract the current version from the first 8 bytes |
||||
|
local currentVersion = tonumber(struct.unpack(">I8", currentVersionBytes)) |
||||
|
|
||||
|
if newVersion >= currentVersion then |
||||
|
setNewValue() |
||||
|
end |
||||
|
else |
||||
|
-- If the current value is absent or the current version is not found, set the new value |
||||
|
setNewValue() |
||||
|
end |
||||
|
"""); |
||||
|
static final byte[] SET_VERSIONED_VALUE_SHA = StringRedisSerializer.UTF_8.serialize("041b109dd56f6c8afb55090076e754727a5d3da0"); |
||||
|
|
||||
|
public VersionedRedisTbTransactionalCache(String cacheName, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory, TBRedisCacheConfiguration configuration, TbRedisSerializer<K, V> valueSerializer) { |
||||
|
super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer); |
||||
|
} |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
try (var connection = getConnection(SET_VERSIONED_VALUE_SHA)) { |
||||
|
log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), connection.getNativeConnection()); |
||||
|
String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT); |
||||
|
if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { |
||||
|
log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), sha, connection.getNativeConnection()); |
||||
|
} |
||||
|
} catch (Throwable t) { |
||||
|
log.error("Error on Redis versioned cache init", t); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected byte[] doGet(RedisConnection connection, byte[] rawKey) { |
||||
|
return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void put(K key, V value) { |
||||
|
Long version = value!= null ? value.getVersion() : 0; |
||||
|
put(key, value, version); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void put(K key, V value, Long version) { |
||||
|
//TODO: use expiration
|
||||
|
log.trace("put [{}][{}][{}]", key, value, version); |
||||
|
if (version == null) { |
||||
|
return; |
||||
|
} |
||||
|
final byte[] rawKey = getRawKey(key); |
||||
|
try (var connection = getConnection(rawKey)) { |
||||
|
byte[] rawValue = getRawValue(value); |
||||
|
byte[] rawVersion = StringRedisSerializer.UTF_8.serialize(String.valueOf(version)); |
||||
|
try { |
||||
|
connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion); |
||||
|
} catch (InvalidDataAccessApiUsageException e) { |
||||
|
log.debug("loading LUA [{}]", connection.getNativeConnection()); |
||||
|
String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT); |
||||
|
if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) { |
||||
|
log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(SET_VERSIONED_VALUE_SHA), sha); |
||||
|
} |
||||
|
try { |
||||
|
connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion); |
||||
|
} catch (InvalidDataAccessApiUsageException ignored) { |
||||
|
log.debug("Slowly executing eval instead of fast evalsha"); |
||||
|
connection.scriptingCommands().eval(SET_VERSIONED_VALUE_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void evict(K key, Long version) { |
||||
|
log.trace("evict [{}][{}]", key, version); |
||||
|
if (version != null) { |
||||
|
//TODO: use evict expiration
|
||||
|
put(key, null, version); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void putIfAbsent(K key, V value) { |
||||
|
log.trace("putIfAbsent [{}][{}]", key, value); |
||||
|
throw new NotImplementedException("putIfAbsent is not supported by versioned cache"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void evict(Collection<K> keys) { |
||||
|
throw new NotImplementedException("evict by many keys is not supported by versioned cache"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void evictOrPut(K key, V value) { |
||||
|
throw new NotImplementedException("evictOrPut is not supported by versioned cache"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TbCacheTransaction<K, V> newTransactionForKey(K key) { |
||||
|
throw new NotImplementedException("newTransactionForKey is not supported by versioned cache"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public TbCacheTransaction<K, V> newTransactionForKeys(List<K> keys) { |
||||
|
throw new NotImplementedException("newTransactionForKeys is not supported by versioned cache"); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.cache; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.HasVersion; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
public interface VersionedTbTransactionalCache<K extends Serializable, V extends Serializable & HasVersion> { |
||||
|
|
||||
|
TbCacheValueWrapper<V> get(K key); |
||||
|
|
||||
|
void put(K key, V value); |
||||
|
|
||||
|
void put(K key, V value, Long version); |
||||
|
|
||||
|
void evict(K key); |
||||
|
|
||||
|
void evict(K key, Long version); |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data; |
||||
|
|
||||
|
public interface HasVersion { |
||||
|
long getVersion(); |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.dao.model; |
||||
|
|
||||
|
public interface BaseVersionedEntity { |
||||
|
long getVersion(); |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.dao.model.sql; |
||||
|
|
||||
|
import jakarta.persistence.Column; |
||||
|
import jakarta.persistence.MappedSuperclass; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; |
||||
|
|
||||
|
@Data |
||||
|
@MappedSuperclass |
||||
|
public abstract class VersionedEntity { |
||||
|
|
||||
|
@Column(name = VERSION_COLUMN) |
||||
|
protected Long version; |
||||
|
} |
||||
Loading…
Reference in new issue