All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.etcd.jetcd.impl.LeaseImpl Maven / Gradle / Ivy

There is a newer version: 0.8.4
Show newest version
/*
 * Copyright 2016-2021 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.impl;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import io.etcd.jetcd.Lease;
import io.etcd.jetcd.api.LeaseGrantRequest;
import io.etcd.jetcd.api.LeaseKeepAliveRequest;
import io.etcd.jetcd.api.LeaseRevokeRequest;
import io.etcd.jetcd.api.LeaseTimeToLiveRequest;
import io.etcd.jetcd.api.VertxLeaseGrpc;
import io.etcd.jetcd.common.Service;
import io.etcd.jetcd.common.exception.ErrorCode;
import io.etcd.jetcd.lease.LeaseGrantResponse;
import io.etcd.jetcd.lease.LeaseKeepAliveResponse;
import io.etcd.jetcd.lease.LeaseRevokeResponse;
import io.etcd.jetcd.lease.LeaseTimeToLiveResponse;
import io.etcd.jetcd.options.LeaseOption;
import io.etcd.jetcd.support.CloseableClient;
import io.etcd.jetcd.support.Util;
import io.grpc.stub.StreamObserver;
import io.vertx.core.streams.WriteStream;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newClosedLeaseClientException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.newEtcdException;
import static io.etcd.jetcd.common.exception.EtcdExceptionFactory.toEtcdException;

/**
 * Implementation of lease client.
 */
final class LeaseImpl extends Impl implements Lease {

    /**
     * if there is no user-provided keep-alive timeout from ClientBuilder, then DEFAULT_FIRST_KEEPALIVE_TIMEOUT_MS
     * is the timeout for the first keepalive request before the actual TTL is known to the lease client.
     */
    private static final int DEFAULT_FIRST_KEEPALIVE_TIMEOUT_MS = 5000;

    private final VertxLeaseGrpc.LeaseVertxStub stub;
    private final VertxLeaseGrpc.LeaseVertxStub leaseStub;
    private final Map keepAlives;
    private final KeepAlive keepAlive;
    private final DeadLine deadLine;
    private volatile boolean closed;

    LeaseImpl(ClientConnectionManager connectionManager) {
        super(connectionManager);

        this.stub = connectionManager().newStub(VertxLeaseGrpc::newVertxStub);
        this.leaseStub = Util.applyRequireLeader(true, connectionManager().newStub(VertxLeaseGrpc::newVertxStub));
        this.keepAlives = new ConcurrentHashMap<>();
        this.keepAlive = new KeepAlive();
        this.deadLine = new DeadLine();
    }

    @Override
    public CompletableFuture grant(long ttl) {
        return execute(
            () -> this.stub.leaseGrant(
                LeaseGrantRequest.newBuilder()
                    .setTTL(ttl)
                    .build()),
            LeaseGrantResponse::new);
    }

    @Override
    public CompletableFuture grant(long ttl, long timeout, TimeUnit unit) {
        return execute(
            () -> this.stub.withDeadlineAfter(timeout, unit).leaseGrant(
                LeaseGrantRequest.newBuilder()
                    .setTTL(ttl)
                    .build()),
            LeaseGrantResponse::new);
    }

    @Override
    public CompletableFuture revoke(long leaseId) {
        return execute(
            () -> this.stub.leaseRevoke(
                LeaseRevokeRequest.newBuilder()
                    .setID(leaseId)
                    .build()),
            LeaseRevokeResponse::new);
    }

    @Override
    public CompletableFuture timeToLive(long leaseId, LeaseOption option) {
        checkNotNull(option, "LeaseOption should not be null");

        LeaseTimeToLiveRequest leaseTimeToLiveRequest = LeaseTimeToLiveRequest.newBuilder()
            .setID(leaseId)
            .setKeys(option.isAttachedKeys())
            .build();

        return execute(
            () -> this.stub.leaseTimeToLive(leaseTimeToLiveRequest),
            LeaseTimeToLiveResponse::new);
    }

    @Override
    public synchronized CloseableClient keepAlive(long leaseId, StreamObserver observer) {
        if (this.closed) {
            throw newClosedLeaseClientException();
        }

        KeepAliveObserver keepAlive = this.keepAlives.computeIfAbsent(leaseId, KeepAliveObserver::new);
        keepAlive.addObserver(observer);

        this.keepAlive.start();
        this.deadLine.start();

        return new CloseableClient() {
            @Override
            public void close() {
                keepAlive.removeObserver(observer);
            }
        };
    }

    @Override
    public CompletableFuture keepAliveOnce(long leaseId) {
        final CompletableFuture future = new CompletableFuture<>();

        final CloseableClient ka = keepAlive(leaseId, new StreamObserver() {
            @Override
            public void onNext(LeaseKeepAliveResponse value) {
                future.complete(value);
            }

            @Override
            public void onError(Throwable t) {
                future.completeExceptionally(toEtcdException(t));
            }

            @Override
            public void onCompleted() {
            }
        });

        return future.whenCompleteAsync(
            (val, throwable) -> ka.close(),
            connectionManager().getExecutorService());
    }

    @Override
    public synchronized void close() {
        if (this.closed) {
            return;
        }
        this.closed = true;

        this.keepAlive.close();
        this.deadLine.close();

        final Throwable errResp = newClosedLeaseClientException();

        this.keepAlives.values().forEach(v -> v.onError(errResp));
        this.keepAlives.clear();
    }

    /**
     * The KeepAliver hold a background task and stream for keep aliaves.
     */
    private final class KeepAlive extends Service {
        private volatile ScheduledFuture task;
        private volatile ScheduledFuture restart;
        private volatile ScheduledExecutorService executor;
        private volatile WriteStream requestStream;

        public KeepAlive() {
            this.executor = Executors.newScheduledThreadPool(2);
        }

        @Override
        public void doStart() {
            leaseStub.leaseKeepAlive(this::writeHandler)
                .handler(this::handleResponse)
                .exceptionHandler(this::handleException);
        }

        @Override
        public void doStop() {
            if (requestStream != null) {
                requestStream.end();
            }
            if (this.restart != null) {
                this.restart.cancel(true);
                this.restart = null;
            }
            if (this.task != null) {
                this.task.cancel(true);
                this.task = null;
            }
        }

        @Override
        public void close() {
            super.close();

            this.task = null;
            this.restart = null;
            this.executor.shutdownNow();
        }

        private void writeHandler(WriteStream stream) {
            requestStream = stream;

            task = executor.scheduleAtFixedRate(
                () -> keepAlives.values().forEach(element -> sendKeepAlive(element, stream)),
                0,
                500,
                TimeUnit.MILLISECONDS);
        }

        private void sendKeepAlive(KeepAliveObserver observer, WriteStream stream) {
            if (observer.getNextKeepAlive() < System.currentTimeMillis()) {
                stream.write(
                    LeaseKeepAliveRequest.newBuilder().setID(observer.getLeaseId()).build());
            }
        }

        private synchronized void handleResponse(io.etcd.jetcd.api.LeaseKeepAliveResponse leaseKeepAliveResponse) {
            if (!this.isRunning()) {
                return;
            }

            final long leaseID = leaseKeepAliveResponse.getID();
            final long ttl = leaseKeepAliveResponse.getTTL();
            final KeepAliveObserver ka = keepAlives.get(leaseID);

            if (ka == null) {
                return;
            }

            if (ttl > 0) {
                long nextKeepAlive = System.currentTimeMillis() + ttl * 1000 / 3;
                ka.setNextKeepAlive(nextKeepAlive);
                ka.setDeadLine(System.currentTimeMillis() + ttl * 1000);
                ka.onNext(leaseKeepAliveResponse);
            } else {
                keepAlives.remove(leaseID);
                ka.onError(newEtcdException(ErrorCode.NOT_FOUND, "etcdserver: requested lease not found"));
            }
        }

        private synchronized void handleException(Throwable r) {
            if (!this.isRunning()) {
                return;
            }

            restart = this.executor.schedule(this::restart, 500, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * The DeadLiner hold a background task to check deadlines.
     */
    private class DeadLine extends Service {
        private volatile ScheduledFuture task;
        private volatile ScheduledExecutorService executor;

        public DeadLine() {
            this.executor = Executors.newScheduledThreadPool(2);
        }

        @Override
        public void doStart() {
            this.task = executor.scheduleAtFixedRate(() -> {
                long now = System.currentTimeMillis();

                keepAlives.values().removeIf(ka -> {
                    if (ka.getDeadLine() < now) {
                        ka.onCompleted();
                        return true;
                    }
                    return false;
                });
            }, 0, 1000, TimeUnit.MILLISECONDS);
        }

        @Override
        public void doStop() {
            if (this.task != null) {
                this.task.cancel(true);
            }
        }

        @Override
        public void close() {
            super.close();

            this.executor.shutdownNow();
        }

    }

    /**
     * The KeepAlive hold the keepAlive information for lease.
     */
    private final class KeepAliveObserver implements StreamObserver {
        private final List> observers;
        private final long leaseId;

        private long deadLine;
        private long nextKeepAlive;

        public KeepAliveObserver(long leaseId) {
            this.nextKeepAlive = System.currentTimeMillis();

            // Use user-provided timeout if present to avoid removing KeepAlive before first response from server
            int initialKeepAliveTimeoutMs = connectionManager().builder().keepaliveTimeout() != null
                ? Math.toIntExact(connectionManager().builder().keepaliveTimeout().toMillis())
                : DEFAULT_FIRST_KEEPALIVE_TIMEOUT_MS;
            this.deadLine = nextKeepAlive + initialKeepAliveTimeoutMs;

            this.observers = new CopyOnWriteArrayList<>();
            this.leaseId = leaseId;
        }

        public long getLeaseId() {
            return leaseId;
        }

        public long getDeadLine() {
            return deadLine;
        }

        public void setDeadLine(long deadLine) {
            this.deadLine = deadLine;
        }

        public void addObserver(StreamObserver observer) {
            this.observers.add(observer);
        }

        public void removeObserver(StreamObserver listener) {
            this.observers.remove(listener);

            if (this.observers.isEmpty()) {
                keepAlives.remove(leaseId);
            }
        }

        public long getNextKeepAlive() {
            return nextKeepAlive;
        }

        public void setNextKeepAlive(long nextKeepAlive) {
            this.nextKeepAlive = nextKeepAlive;
        }

        @Override
        public void onNext(io.etcd.jetcd.api.LeaseKeepAliveResponse response) {
            for (StreamObserver observer : observers) {
                observer.onNext(new LeaseKeepAliveResponse(response));
            }
        }

        @Override
        public void onError(Throwable throwable) {
            for (StreamObserver observer : observers) {
                observer.onError(toEtcdException(throwable));
            }
        }

        @Override
        public void onCompleted() {
            this.observers.forEach(StreamObserver::onCompleted);
            this.observers.clear();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy