forked from juanjuandog/FinSight-AI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRedisBackedWorkflowLeaseService.java
More file actions
157 lines (147 loc) · 6.33 KB
/
Copy pathRedisBackedWorkflowLeaseService.java
File metadata and controls
157 lines (147 loc) · 6.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package com.finsight.workflow;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class RedisBackedWorkflowLeaseService implements WorkflowLeaseService {
private static final String ACQUIRE_LUA = """
local leaseKey = KEYS[1]
local fenceKey = KEYS[2]
local owner = ARGV[1]
local ttlMillis = tonumber(ARGV[2])
if redis.call('exists', leaseKey) == 0 then
local token = redis.call('incr', fenceKey)
redis.call('psetex', leaseKey, ttlMillis, owner .. ':' .. token)
return token
end
return nil
""";
private static final String RELEASE_LUA = """
local leaseKey = KEYS[1]
local expected = ARGV[1]
if redis.call('get', leaseKey) == expected then
return redis.call('del', leaseKey)
end
return 0
""";
private static final String RENEW_LUA = """
local leaseKey = KEYS[1]
local expected = ARGV[1]
local ttlMillis = tonumber(ARGV[2])
if redis.call('get', leaseKey) == expected then
redis.call('pexpire', leaseKey, ttlMillis)
return 1
end
return 0
""";
private final StringRedisTemplate redisTemplate;
private final String ownerPrefix;
private final boolean allowLocalFallback;
private final ConcurrentHashMap<String, WorkflowLease> localLeases = new ConcurrentHashMap<>();
private final AtomicLong localFence = new AtomicLong();
public RedisBackedWorkflowLeaseService(
ObjectProvider<StringRedisTemplate> redisTemplate,
@Value("${finsight.workflow.lease-owner:}") String configuredOwner,
@Value("${finsight.workflow.allow-local-lease-fallback:true}") boolean allowLocalFallback
) {
this.redisTemplate = redisTemplate.getIfAvailable();
this.allowLocalFallback = allowLocalFallback;
this.ownerPrefix = configuredOwner == null || configuredOwner.isBlank()
? ManagementFactory.getRuntimeMXBean().getName()
: configuredOwner;
}
@Override
public Optional<WorkflowLease> tryAcquire(String key, Duration ttl) {
String owner = ownerPrefix + ":" + UUID.randomUUID();
Instant expiresAt = Instant.now().plus(ttl);
if (redisTemplate != null) {
try {
Long token = redisTemplate.execute(
new DefaultRedisScript<>(ACQUIRE_LUA, Long.class),
List.of(redisKey(key), redisFenceKey(key)),
owner,
String.valueOf(ttl.toMillis())
);
return token == null ? Optional.empty() : Optional.of(new WorkflowLease(key, owner, token, expiresAt));
} catch (RuntimeException ex) {
if (!allowLocalFallback) {
throw new IllegalStateException("Redis lease acquisition failed and local fallback is disabled", ex);
}
}
}
if (!allowLocalFallback) {
throw new IllegalStateException("Redis lease service is unavailable and local fallback is disabled");
}
WorkflowLease lease = new WorkflowLease(key, owner, localFence.incrementAndGet(), expiresAt);
WorkflowLease existing = localLeases.compute(key, (ignored, current) -> {
if (current == null || current.expiresAt().isBefore(Instant.now())) {
return lease;
}
return current;
});
return lease.equals(existing) ? Optional.of(lease) : Optional.empty();
}
@Override
public Optional<WorkflowLease> renew(WorkflowLease lease, Duration ttl) {
WorkflowLease renewed = new WorkflowLease(
lease.key(),
lease.owner(),
lease.fencingToken(),
Instant.now().plus(ttl)
);
if (redisTemplate != null) {
try {
Long result = redisTemplate.execute(
new DefaultRedisScript<>(RENEW_LUA, Long.class),
List.of(redisKey(lease.key())),
lease.owner() + ":" + lease.fencingToken(),
String.valueOf(ttl.toMillis())
);
return Long.valueOf(1).equals(result) ? Optional.of(renewed) : Optional.empty();
} catch (RuntimeException ex) {
if (!allowLocalFallback) {
throw new IllegalStateException("Redis lease renewal failed and local fallback is disabled", ex);
}
}
}
if (!allowLocalFallback) {
throw new IllegalStateException("Redis lease service is unavailable and local fallback is disabled");
}
boolean replaced = localLeases.replace(lease.key(), lease, renewed);
return replaced ? Optional.of(renewed) : Optional.empty();
}
@Override
public void release(WorkflowLease lease) {
if (redisTemplate != null) {
try {
redisTemplate.execute(
new DefaultRedisScript<>(RELEASE_LUA, Long.class),
List.of(redisKey(lease.key())),
lease.owner() + ":" + lease.fencingToken()
);
return;
} catch (RuntimeException ex) {
if (!allowLocalFallback) {
throw new IllegalStateException("Redis lease release failed and local fallback is disabled", ex);
}
}
}
localLeases.remove(lease.key(), lease);
}
private String redisKey(String key) {
return "finsight:workflow:lease:" + key;
}
private String redisFenceKey(String key) {
return "finsight:workflow:fence:" + key;
}
}