
io.etcd.jetcd.WatchImpl Maven / Gradle / Ivy
The newest version!
/**
* Copyright 2017 The jetcd 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 io.etcd.jetcd;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newClosedWatchClientException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newCompactedException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newEtcdException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import io.etcd.jetcd.api.WatchCreateRequest;
import io.etcd.jetcd.api.WatchGrpc;
import io.etcd.jetcd.api.WatchRequest;
import io.etcd.jetcd.api.WatchResponse;
import io.etcd.jetcd.common.exception.ErrorCode;
import io.etcd.jetcd.options.WatchOption;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* watch Implementation.
*/
final class WatchImpl implements Watch {
private static final Logger LOG = LoggerFactory.getLogger(WatchImpl.class);
private final Object lock;
private final ClientConnectionManager connectionManager;
private final WatchGrpc.WatchStub stub;
private final ListeningScheduledExecutorService executor;
private final AtomicBoolean closed;
private final List watchers;
WatchImpl(ClientConnectionManager connectionManager) {
this.lock = new Object();
this.connectionManager = connectionManager;
this.stub = connectionManager.newStub(WatchGrpc::newStub);
this.executor = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1));
this.closed = new AtomicBoolean();
this.watchers = new ArrayList<>();
}
@Override
public Watcher watch(ByteSequence key, WatchOption option, Listener listener) {
if (closed.get()) {
throw newClosedWatchClientException();
}
WatcherImpl impl;
synchronized (this.lock) {
impl = new WatcherImpl(key, option, listener);
impl.resume();
watchers.add(impl);
}
return impl;
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
synchronized (this.lock) {
executor.shutdownNow();
watchers.forEach(Watcher::close);
}
}
}
private final class WatcherImpl implements Watcher, StreamObserver {
private final ByteSequence key;
private final WatchOption option;
private final Listener listener;
private final AtomicBoolean closed;
private StreamObserver stream;
private long revision;
private long id;
WatcherImpl(ByteSequence key, WatchOption option, Listener listener) {
this.key = key;
this.option = option;
this.listener = listener;
this.closed = new AtomicBoolean();
this.stream = null;
this.id = -1;
this.revision = this.option.getRevision();
}
// ************************
//
// Lifecycle
//
// ************************
void resume() {
if (this.closed.get() || WatchImpl.this.closed.get()) {
return;
}
if (stream == null) {
// id is not really useful today but it may be in etcd 3.4
id = -1;
WatchCreateRequest.Builder builder = WatchCreateRequest.newBuilder()
.setKey(this.key.getByteString())
.setPrevKv(this.option.isPrevKV())
.setProgressNotify(option.isProgressNotify())
.setStartRevision(this.revision);
option.getEndKey()
.map(ByteSequence::getByteString)
.ifPresent(builder::setRangeEnd);
if (option.isNoDelete()) {
builder.addFilters(WatchCreateRequest.FilterType.NODELETE);
}
if (option.isNoPut()) {
builder.addFilters(WatchCreateRequest.FilterType.NOPUT);
}
stream = stub.watch(this);
stream.onNext(WatchRequest.newBuilder().setCreateRequest(builder).build());
}
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
if (stream != null) {
stream.onCompleted();
stream = null;
}
id = -1;
listener.onCompleted();
}
}
// ************************
//
// StreamObserver
//
// ************************
@Override
public void onNext(WatchResponse response) {
if (response.getCreated()) {
//
// Created
//
if (response.getWatchId() == -1) {
listener.onError(newEtcdException(ErrorCode.INTERNAL, "etcd server failed to create watch id"));
return;
}
revision = response.getHeader().getRevision();
id = response.getWatchId();
} else if (response.getCanceled()) {
//
// Cancelled
//
String reason = response.getCancelReason();
Throwable error;
if (response.getCompactRevision() != 0) {
error = newCompactedException(response.getCompactRevision());
} else if (Strings.isNullOrEmpty(reason)) {
error = newEtcdException(
ErrorCode.OUT_OF_RANGE,
"etcdserver: mvcc: required revision is a future revision"
);
} else {
error = newEtcdException(
ErrorCode.FAILED_PRECONDITION,
reason
);
}
listener.onError(error);
} else if (response.getEventsCount() == 0 && option.isProgressNotify()) {
//
// Event
//
// A response may not contain events, this is in case of "Progress_Notify":
//
// the watch will periodically receive a WatchResponse with no events,
// if there are no recent events. It is useful when clients wish to
// recover a disconnected watcher starting from a recent known revision.
//
// The etcd server decides how often to send notifications based on current
// server load.
//
// For more info:
// https://coreos.com/etcd/docs/latest/learning/api.html#watch-streams
//
listener.onNext(new io.etcd.jetcd.watch.WatchResponse(response));
revision = response.getHeader().getRevision();
} else if (response.getEventsCount() > 0) {
listener.onNext(new io.etcd.jetcd.watch.WatchResponse(response));
revision = response.getEvents(response.getEventsCount() - 1).getKv().getModRevision() + 1;
}
}
@Override
public void onError(Throwable t) {
if (this.closed.get() || WatchImpl.this.closed.get()) {
return;
}
Status status = Status.fromThrowable(t);
if (Util.isHaltError(status) || Util.isNoLeaderError(status)) {
listener.onError(toEtcdException(status));
}
stream.onCompleted();
stream = null;
// resume with a delay; avoiding immediate retry on a long connection downtime.
Util.addOnFailureLoggingCallback(
WatchImpl.this.connectionManager.getExecutorService(),
executor.schedule(this::resume, 500, TimeUnit.MILLISECONDS),
LOG,
"scheduled resume failed"
);
}
@Override
public void onCompleted() {
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy