Skip to content

Commit 057b390

Browse files
authored
Merge branch 'master' into task/improve-condition-query-semantics
2 parents 18fe87f + 86e0a66 commit 057b390

19 files changed

Lines changed: 1116 additions & 79 deletions

File tree

.github/workflows/server-ci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ jobs:
106106
107107
- name: Run api test
108108
run: |
109-
$TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR
109+
if [ "$BACKEND" = "rocksdb" ]; then
110+
$TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR true
111+
else
112+
$TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR
113+
fi
110114
111115
# TODO: disable raft test in normal PR due to the always timeout problem
112116
- name: Run raft test
@@ -180,7 +184,7 @@ jobs:
180184
181185
- name: Run RocksDB API test
182186
run: |
183-
$TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR
187+
$TRAVIS_DIR/run-api-test.sh $BACKEND $REPORT_DIR true
184188
185189
- name: Show server log on failure
186190
if: failure()

hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/job/TaskAPI.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,13 @@ public Map<String, Object> list(@Context GraphManager manager,
102102
limit = NO_LIMIT;
103103
List<Id> idList = ids.stream().map(IdGenerator::of)
104104
.collect(Collectors.toList());
105-
iter = scheduler.tasks(idList);
105+
iter = scheduler.tasks(idList, false);
106106
} else {
107107
if (status == null) {
108-
iter = scheduler.tasks(null, limit, page);
108+
iter = scheduler.tasks(null, limit, page, false);
109109
} else {
110-
iter = scheduler.tasks(parseStatus(status), limit, page);
110+
iter = scheduler.tasks(parseStatus(status), limit, page,
111+
false);
111112
}
112113
}
113114

@@ -136,12 +137,17 @@ public Map<String, Object> get(@Context GraphManager manager,
136137
@Parameter(description = "The graph name")
137138
@PathParam("graph") String graph,
138139
@Parameter(description = "The task id")
139-
@PathParam("id") long id) {
140+
@PathParam("id") long id,
141+
@Parameter(description = "Whether to load task result")
142+
@DefaultValue("true")
143+
@QueryParam("with_result")
144+
boolean withResult) {
140145
LOG.debug("Graph [{}] get task: {}", graph, id);
141146

142147
TaskScheduler scheduler = graph(manager, graphSpace, graph)
143148
.taskScheduler();
144-
return scheduler.task(IdGenerator.of(id)).asMap();
149+
return scheduler.task(IdGenerator.of(id), withResult)
150+
.asMap(true, withResult);
145151
}
146152

147153
@DELETE

hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,8 +1326,14 @@ public <V> void save(HugeTask<V> task) {
13261326

13271327
@Override
13281328
public <V> HugeTask<V> task(Id id) {
1329+
return this.task(id, true);
1330+
}
1331+
1332+
@Override
1333+
public <V> HugeTask<V> task(Id id, boolean withResult) {
13291334
return verifyTaskPermission(HugePermission.READ,
1330-
this.taskScheduler.task(id));
1335+
this.taskScheduler.task(id,
1336+
withResult));
13311337
}
13321338

13331339
@Override
@@ -1336,18 +1342,36 @@ public <V> Iterator<HugeTask<V>> tasks(List<Id> ids) {
13361342
this.taskScheduler.tasks(ids));
13371343
}
13381344

1345+
@Override
1346+
public <V> Iterator<HugeTask<V>> tasks(List<Id> ids,
1347+
boolean withResult) {
1348+
return verifyTaskPermission(HugePermission.READ,
1349+
this.taskScheduler.tasks(ids,
1350+
withResult));
1351+
}
1352+
13391353
@Override
13401354
public <V> Iterator<HugeTask<V>> tasks(TaskStatus status,
13411355
long limit, String page) {
13421356
Iterator<HugeTask<V>> tasks = this.taskScheduler.tasks(status,
1343-
limit, page);
1357+
limit,
1358+
page);
1359+
return verifyTaskPermission(HugePermission.READ, tasks);
1360+
}
1361+
1362+
@Override
1363+
public <V> Iterator<HugeTask<V>> tasks(TaskStatus status,
1364+
long limit, String page,
1365+
boolean withResult) {
1366+
Iterator<HugeTask<V>> tasks = this.taskScheduler.tasks(
1367+
status, limit, page, withResult);
13441368
return verifyTaskPermission(HugePermission.READ, tasks);
13451369
}
13461370

13471371
@Override
13481372
public <V> HugeTask<V> delete(Id id, boolean force) {
13491373
verifyTaskPermission(HugePermission.DELETE,
1350-
this.taskScheduler.task(id));
1374+
this.taskScheduler.task(id, false));
13511375
return this.taskScheduler.delete(id, force);
13521376
}
13531377

hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/cache/CachedSchemaTransactionV2.java

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ public class CachedSchemaTransactionV2 extends SchemaTransactionV2 {
5252

5353
// MetaDriver doesn't expose unlisten, register the meta listener once.
5454
// Lifecycle: this JVM-global flag is intentionally never reset by
55-
// unlistenChanges() (the underlying gRPC watch is process-wide). If that
56-
// watch is silently dropped after a transport reconnect, recovery is not
57-
// automatic; resetMetaListenerForReconnect() is only a manual hook to let
58-
// the next schema operation install a fresh watch.
55+
// unlistenChanges() (the underlying gRPC watch is process-wide). The driver
56+
// watch self-heals across transport reconnects (PdMetaDriver via KvClient,
57+
// EtcdMetaDriver via Watch.Listener re-subscribe), so the subscription stays
58+
// live and the flag staying true is correct.
5959
private static final AtomicBoolean metaEventListenerRegistered =
6060
new AtomicBoolean(false);
6161

@@ -251,27 +251,6 @@ static <T> void handleSchemaCacheClearEvent(T response) {
251251
}
252252
}
253253

254-
/**
255-
* Manually reset the JVM-global meta listener flag after detecting that
256-
* the MetaManager transport reconnected and dropped the underlying gRPC
257-
* watch. This method is not wired to a MetaManager/MetaDriver reconnect
258-
* callback today; callers must invoke it explicitly after detecting that
259-
* condition. Without such a manual reset {@link #metaEventListenerRegistered}
260-
* would stay {@code true} forever and this JVM would stop receiving
261-
* cross-node schema cache clear events with no error or warning.
262-
*
263-
* <p>TODO: wire this into MetaManager once it exposes a transport
264-
* reconnect callback (e.g. {@code listenReconnect} /
265-
* {@code onTransportReconnect}). Until then it must be invoked
266-
* explicitly by code that detects the reconnect.
267-
*/
268-
public static void resetMetaListenerForReconnect() {
269-
if (metaEventListenerRegistered.compareAndSet(true, false)) {
270-
LOG.warn("Schema cache clear meta listener lost on reconnect - " +
271-
"will re-register on next schema operation.");
272-
}
273-
}
274-
275254
public void clearCache(boolean notify) {
276255
// Same TOCTOU ordering as clearSchemaCache(String): clear nameCache
277256
// first, then the array attachment, then idCache last.

hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/meta/EtcdMetaDriver.java

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import java.util.List;
2626
import java.util.Map;
2727
import java.util.concurrent.ExecutionException;
28+
import java.util.concurrent.Executors;
29+
import java.util.concurrent.ScheduledExecutorService;
30+
import java.util.concurrent.TimeUnit;
2831
import java.util.function.Consumer;
2932

3033
import org.apache.commons.io.FileUtils;
@@ -33,7 +36,9 @@
3336
import org.apache.hugegraph.meta.lock.LockResult;
3437
import org.apache.hugegraph.type.define.CollectionType;
3538
import org.apache.hugegraph.util.E;
39+
import org.apache.hugegraph.util.Log;
3640
import org.apache.hugegraph.util.collection.CollectionFactory;
41+
import org.slf4j.Logger;
3742

3843
import com.google.common.base.Strings;
3944

@@ -42,6 +47,7 @@
4247
import io.etcd.jetcd.ClientBuilder;
4348
import io.etcd.jetcd.KV;
4449
import io.etcd.jetcd.KeyValue;
50+
import io.etcd.jetcd.Watch;
4551
import io.etcd.jetcd.kv.GetResponse;
4652
import io.etcd.jetcd.lease.LeaseKeepAliveResponse;
4753
import io.etcd.jetcd.options.DeleteOption;
@@ -57,9 +63,24 @@
5763

5864
public class EtcdMetaDriver implements MetaDriver {
5965

66+
private static final Logger LOG = Log.logger(EtcdMetaDriver.class);
67+
6068
private final Client client;
6169
private final EtcdDistributedLock lock;
6270

71+
// Re-subscribes a dropped watch off the jetcd callback thread; single
72+
// daemon thread, process-lifetime (no close()), so JVM shutdown reclaims it.
73+
private final ScheduledExecutorService reWatchExecutor =
74+
Executors.newSingleThreadScheduledExecutor(r -> {
75+
Thread thread = new Thread(r, "etcd-meta-rewatch");
76+
thread.setDaemon(true);
77+
return thread;
78+
});
79+
80+
// Backoff before re-subscribing a dropped watch. Package-private and
81+
// mutable only so tests can set it to 0; never reassigned in production.
82+
long reWatchDelayMs = 1000L;
83+
6384
public EtcdMetaDriver(String trustFile, String clientCertFile,
6485
String clientKeyFile, Object... endpoints) {
6586
ClientBuilder builder = this.etcdMetaDriverBuilder(endpoints);
@@ -76,6 +97,13 @@ public EtcdMetaDriver(Object... endpoints) {
7697
this.lock = EtcdDistributedLock.getInstance(this.client);
7798
}
7899

100+
// Package-private constructor for tests: inject a mock Client and skip lock
101+
// setup (watch tests never touch the distributed lock).
102+
EtcdMetaDriver(Client client) {
103+
this.client = client;
104+
this.lock = null;
105+
}
106+
79107
private static ByteSequence toByteSequence(String content) {
80108
return ByteSequence.from(content.getBytes());
81109
}
@@ -303,9 +331,8 @@ public void unlock(String key, LockResult lockResult) {
303331
@SuppressWarnings("unchecked")
304332
@Override
305333
public <T> void listen(String key, Consumer<T> consumer) {
306-
307-
this.client.getWatchClient().watch(toByteSequence(key),
308-
(Consumer<WatchResponse>) consumer);
334+
this.watchKey(toByteSequence(key), WatchOption.DEFAULT,
335+
(Consumer<WatchResponse>) consumer);
309336
}
310337

311338
/**
@@ -314,9 +341,63 @@ public <T> void listen(String key, Consumer<T> consumer) {
314341
@SuppressWarnings("unchecked")
315342
@Override
316343
public <T> void listenPrefix(String prefix, Consumer<T> consumer) {
317-
ByteSequence sequence = toByteSequence(prefix);
318344
WatchOption option = WatchOption.newBuilder().isPrefix(true).build();
319-
this.client.getWatchClient().watch(sequence, option, (Consumer<WatchResponse>) consumer);
345+
this.watchKey(toByteSequence(prefix), option,
346+
(Consumer<WatchResponse>) consumer);
347+
}
348+
349+
/**
350+
* Subscribe a watch that survives the terminal close jetcd cannot recover
351+
* from. The bare {@code Consumer} overload discards {@code onError} and
352+
* {@code onCompleted}, so a non-retryable failure silently drops the
353+
* listener (issue #3036).
354+
* <p>
355+
* jetcd 0.5.9 ({@code WatchImpl.WatcherImpl.handleError}) already retries
356+
* <em>retryable</em> errors itself: it notifies {@code onError} and then
357+
* reschedules {@code resume()} on the same watcher. Re-subscribing from
358+
* {@code onError} would therefore open a duplicate watch on every transient
359+
* reconnect, so {@code onError} only logs here. A non-retryable error (or an
360+
* explicit cancel) ends in {@code close()}, which removes the watcher and
361+
* invokes {@code onCompleted}; that is the only point where the watch is
362+
* truly gone, so re-subscribe happens there. The old watcher is already
363+
* closed and removed, so the replacement is not a duplicate.
364+
*/
365+
private void watchKey(ByteSequence key, WatchOption option,
366+
Consumer<WatchResponse> consumer) {
367+
Watch.Listener listener = Watch.listener(
368+
consumer,
369+
throwable -> LOG.warn("etcd meta watch error for key '{}', " +
370+
"jetcd will retry if recoverable",
371+
key.toString(Charset.defaultCharset()),
372+
throwable),
373+
() -> this.scheduleReWatch(key, option, consumer));
374+
this.client.getWatchClient().watch(key, option, listener);
375+
}
320376

377+
private void scheduleReWatch(ByteSequence key, WatchOption option,
378+
Consumer<WatchResponse> consumer) {
379+
this.reWatchExecutor.schedule(() -> this.reWatch(key, option, consumer),
380+
this.reWatchDelayMs, TimeUnit.MILLISECONDS);
381+
}
382+
383+
/**
384+
* Re-establish a watch dropped by a terminal close. If the re-subscribe
385+
* itself fails (e.g. the endpoint is still unreachable), it is retried with
386+
* the same backoff instead of giving up, otherwise a single failed attempt
387+
* would lose the listener permanently.
388+
*/
389+
private void reWatch(ByteSequence key, WatchOption option,
390+
Consumer<WatchResponse> consumer) {
391+
try {
392+
LOG.info("Re-establishing etcd meta watch for key '{}'",
393+
key.toString(Charset.defaultCharset()));
394+
this.watchKey(key, option, consumer);
395+
} catch (Exception e) {
396+
LOG.warn("Failed to re-establish etcd meta watch for key '{}', " +
397+
"retrying in {} ms",
398+
key.toString(Charset.defaultCharset()),
399+
this.reWatchDelayMs, e);
400+
this.scheduleReWatch(key, option, consumer);
401+
}
321402
}
322403
}

hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/task/DistributedTaskScheduler.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,14 @@ protected <V> HugeTask<V> deleteFromDB(Id id) {
306306
Iterator<Vertex> vertices = this.tx().queryTaskInfos(id);
307307
HugeVertex vertex = (HugeVertex) QueryResults.one(vertices);
308308
if (vertex == null) {
309+
this.deleteTaskResultFromTx(id);
309310
return null;
310311
}
311-
HugeTask<V> result = HugeTask.fromVertex(vertex);
312-
this.tx().removeVertex(vertex);
312+
HugeTask<V> result = HugeTask.fromVertex(vertex, false);
313+
// Keep the task vertex as a retryable tombstone until its result
314+
// vertex is removed; cronSchedule() can rediscover DELETING tasks.
315+
this.deleteTaskResultFromTx(id);
316+
this.tx().removeTaskVertex(vertex);
313317
return result;
314318
});
315319
}
@@ -322,6 +326,12 @@ public <V> HugeTask<V> delete(Id id, boolean force) {
322326
this.updateStatus(id, null, TaskStatus.DELETING);
323327
return null;
324328
} else {
329+
HugeTask<?> task = this.taskWithoutResult(id);
330+
if (task != null && task.status() != TaskStatus.DELETING) {
331+
initTaskParams(task);
332+
task.overwriteStatus(TaskStatus.DELETING);
333+
this.save(task);
334+
}
325335
return this.deleteFromDB(id);
326336
}
327337
}
@@ -587,7 +597,7 @@ private void unlockTask(String taskId, LockResult lockResult) {
587597
}
588598
}
589599

590-
private boolean isLockedTask(String taskId) {
600+
protected boolean isLockedTask(String taskId) {
591601
return MetaManager.instance().isLockedTask(graphSpace,
592602
graphName, taskId);
593603
}
@@ -629,7 +639,7 @@ public void run() {
629639
// 1. start task can be from schedule() & cronSchedule()
630640
// 2. recheck the status of task, in case one same task
631641
// called by both methods at same time;
632-
HugeTask<Object> queryTask = task(this.task.id());
642+
HugeTask<Object> queryTask = task(this.task.id(), false);
633643
if (queryTask != null &&
634644
!TaskStatus.NEW.equals(queryTask.status())) {
635645
return;

0 commit comments

Comments
 (0)