| /* |
| * Copyright 2023 CeresDB Project Authors. Licensed under Apache-2.0. |
| */ |
| package io.ceresdb; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import io.ceresdb.common.Display; |
| import io.ceresdb.common.Endpoint; |
| import io.ceresdb.common.Lifecycle; |
| import io.ceresdb.common.signal.SignalHandlersLoader; |
| import io.ceresdb.common.util.MetricExecutor; |
| import io.ceresdb.common.util.MetricsUtil; |
| import io.ceresdb.models.Err; |
| import io.ceresdb.models.Point; |
| import io.ceresdb.models.RequestContext; |
| import io.ceresdb.models.SqlQueryOk; |
| import io.ceresdb.models.SqlQueryRequest; |
| import io.ceresdb.models.Result; |
| import io.ceresdb.models.WriteOk; |
| import io.ceresdb.models.WriteRequest; |
| import io.ceresdb.options.CeresDBOptions; |
| import io.ceresdb.options.QueryOptions; |
| import io.ceresdb.options.RouterOptions; |
| import io.ceresdb.options.WriteOptions; |
| import io.ceresdb.rpc.Context; |
| import io.ceresdb.rpc.Observer; |
| import io.ceresdb.rpc.RpcClient; |
| import io.ceresdb.rpc.RpcFactoryProvider; |
| import io.ceresdb.rpc.RpcOptions; |
| import io.ceresdb.util.RpcServiceRegister; |
| import io.ceresdb.util.StreamWriteBuf; |
| import io.ceresdb.util.Utils; |
| |
| import com.codahale.metrics.Counter; |
| import com.codahale.metrics.Meter; |
| |
| /** |
| * CeresDB client. |
| * |
| */ |
| public class CeresDBClient implements Write, Query, Lifecycle<CeresDBOptions>, Display { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(CeresDBClient.class); |
| |
| private static final Map<Integer, CeresDBClient> INSTANCES = new ConcurrentHashMap<>(); |
| private static final AtomicInteger ID = new AtomicInteger(0); |
| private static final String ID_KEY = "client.id"; |
| private static final String VERSION_KEY = "client.version"; |
| private static final String VERSION = loadVersion(); |
| |
| private final int id; |
| private final AtomicBoolean started = new AtomicBoolean(false); |
| |
| private CeresDBOptions opts; |
| private RouterClient routerClient; |
| private WriteClient writeClient; |
| private QueryClient queryClient; |
| |
| // Note: We do not close it to free resources, as we view it as shared |
| private Executor asyncWritePool; |
| private Executor asyncReadPool; |
| |
| static { |
| // load all signal handlers |
| SignalHandlersLoader.load(); |
| // register all rpc service |
| RpcServiceRegister.registerStorageService(); |
| // start scheduled metric reporter |
| MetricsUtil.startScheduledReporter(Utils.autoReportPeriodMin(), TimeUnit.MINUTES); |
| Runtime.getRuntime().addShutdownHook(new Thread(MetricsUtil::stopScheduledReporterAndDestroy)); |
| } |
| |
| public CeresDBClient() { |
| this.id = ID.incrementAndGet(); |
| } |
| |
| @Override |
| public boolean init(final CeresDBOptions opts) { |
| if (!this.started.compareAndSet(false, true)) { |
| throw new IllegalStateException("CeresDB client has started"); |
| } |
| |
| this.opts = CeresDBOptions.check(opts).copy(); |
| |
| final RpcClient rpcClient = initRpcClient(this.opts); |
| this.routerClient = initRouteClient(this.opts, rpcClient); |
| this.asyncWritePool = withMetricPool(this.opts.getAsyncWritePool(), "async_write_pool.time"); |
| this.asyncReadPool = withMetricPool(this.opts.getAsyncReadPool(), "async_read_pool.time"); |
| this.writeClient = initWriteClient(this.opts, this.routerClient, this.asyncWritePool); |
| this.queryClient = initQueryClient(this.opts, this.routerClient, this.asyncReadPool); |
| |
| INSTANCES.put(this.id, this); |
| |
| Utils.scheduleDisplaySelf(this, new LogPrinter(LOG)); |
| |
| return true; |
| } |
| |
| @Override |
| public void shutdownGracefully() { |
| if (!this.started.compareAndSet(true, false)) { |
| return; |
| } |
| |
| if (this.writeClient != null) { |
| this.writeClient.shutdownGracefully(); |
| } |
| |
| if (this.queryClient != null) { |
| this.queryClient.shutdownGracefully(); |
| } |
| |
| if (this.routerClient != null) { |
| this.routerClient.shutdownGracefully(); |
| } |
| |
| INSTANCES.remove(this.id); |
| } |
| |
| @Override |
| public void ensureInitialized() { |
| if (this.started.get() && INSTANCES.containsKey(this.id)) { |
| return; |
| } |
| throw new IllegalStateException(String.format("CeresDBClient(%d) is not started", this.id)); |
| } |
| |
| @Override |
| public CompletableFuture<Result<WriteOk, Err>> write(final WriteRequest req, final Context ctx) { |
| ensureInitialized(); |
| return this.writeClient.write(req, attachCtx(ctx)); |
| } |
| |
| @Override |
| public StreamWriteBuf<Point, WriteOk> streamWrite(RequestContext reqCtx, final String table, final Context ctx) { |
| ensureInitialized(); |
| return this.writeClient.streamWrite(reqCtx, table, attachCtx(ctx)); |
| } |
| |
| @Override |
| public CompletableFuture<Result<SqlQueryOk, Err>> sqlQuery(final SqlQueryRequest req, final Context ctx) { |
| ensureInitialized(); |
| return this.queryClient.sqlQuery(req, attachCtx(ctx)); |
| } |
| |
| @Override |
| public void streamSqlQuery(final SqlQueryRequest req, final Context ctx, final Observer<SqlQueryOk> observer) { |
| ensureInitialized(); |
| this.queryClient.streamSqlQuery(req, attachCtx(ctx), observer); |
| } |
| |
| public static List<CeresDBClient> instances() { |
| return new ArrayList<>(INSTANCES.values()); |
| } |
| |
| public int id() { |
| return this.id; |
| } |
| |
| public String version() { |
| return VERSION; |
| } |
| |
| public RouterClient routerClient() { |
| return this.routerClient; |
| } |
| |
| @Override |
| public void display(final Printer out) { |
| out.println("--- CeresDBClient ---") // |
| .print("id=") // |
| .println(this.id) // |
| .print("version=") // |
| .println(version()) // |
| .print("clusterAddress=") // |
| .println(this.opts.getClusterAddress()) // |
| .print("database=") // |
| .println(this.opts.getDatabase()) // |
| .print("userAsyncWritePool=") // |
| .println(this.opts.getAsyncWritePool()) // |
| .print("userAsyncReadPool=") // |
| .println(this.opts.getAsyncReadPool()); |
| |
| if (this.routerClient != null) { |
| out.println(""); |
| this.routerClient.display(out); |
| } |
| |
| if (this.writeClient != null) { |
| out.println(""); |
| this.writeClient.display(out); |
| } |
| |
| if (this.queryClient != null) { |
| out.println(""); |
| this.queryClient.display(out); |
| } |
| |
| out.println(""); |
| } |
| |
| @Override |
| public String toString() { |
| return "CeresDBClient{" + // |
| "id=" + id + // |
| ", version=" + version() + // |
| ", started=" + started + // |
| ", opts=" + opts + // |
| ", writeClient=" + writeClient + // |
| ", asyncWritePool=" + asyncWritePool + // |
| ", asyncReadPool=" + asyncReadPool + // |
| '}'; |
| } |
| |
| private Executor withMetricPool(final Executor pool, final String name) { |
| return pool == null ? null : new MetricExecutor(pool, name); |
| } |
| |
| private Context attachCtx(final Context ctx) { |
| final Context c = ctx == null ? Context.newDefault() : ctx; |
| return c.with(ID_KEY, id()).with(VERSION_KEY, version()); |
| } |
| |
| private static RpcClient initRpcClient(final CeresDBOptions opts) { |
| final RpcOptions rpcOpts = opts.getRpcOptions(); |
| final RpcClient rpcClient = RpcFactoryProvider.getRpcFactory().createRpcClient(); |
| if (!rpcClient.init(rpcOpts)) { |
| throw new IllegalStateException("Fail to start RPC client"); |
| } |
| rpcClient.registerConnectionObserver(new RpcConnectionObserver()); |
| return rpcClient; |
| } |
| |
| private static RouterClient initRouteClient(final CeresDBOptions opts, final RpcClient rpcClient) { |
| final RouterOptions routerOpts = opts.getRouterOptions(); |
| routerOpts.setRpcClient(rpcClient); |
| |
| RouterClient routerClient; |
| switch (routerOpts.getRouteMode()) { |
| case DIRECT: |
| routerClient = new RouterClient(); |
| break; |
| case PROXY: |
| routerClient = new ProxyRouterClient(); |
| break; |
| default: |
| throw new IllegalArgumentException("Invalid Route mode " + routerOpts.getRouteMode()); |
| } |
| if (!routerClient.init(routerOpts)) { |
| throw new IllegalStateException("Fail to start router client"); |
| } |
| return routerClient; |
| } |
| |
| private static WriteClient initWriteClient(final CeresDBOptions opts, // |
| final RouterClient routerClient, // |
| final Executor asyncPool) { |
| final WriteOptions writeOpts = opts.getWriteOptions(); |
| writeOpts.setDatabase(opts.getDatabase()); |
| writeOpts.setRoutedClient(routerClient); |
| writeOpts.setAsyncPool(asyncPool); |
| final WriteClient writeClient = new WriteClient(); |
| if (!writeClient.init(writeOpts)) { |
| throw new IllegalStateException("Fail to start write client"); |
| } |
| return writeClient; |
| } |
| |
| private static QueryClient initQueryClient(final CeresDBOptions opts, // |
| final RouterClient routerClient, // |
| final Executor asyncPool) { |
| final QueryOptions queryOpts = opts.getQueryOptions(); |
| queryOpts.setDatabase(opts.getDatabase()); |
| queryOpts.setRouterClient(routerClient); |
| queryOpts.setAsyncPool(asyncPool); |
| final QueryClient queryClient = new QueryClient(); |
| if (!queryClient.init(queryOpts)) { |
| throw new IllegalStateException("Fail to start query client"); |
| } |
| return queryClient; |
| } |
| |
| private static String loadVersion() { |
| try { |
| return Utils // |
| .loadProperties(CeresDBClient.class.getClassLoader(), "client_version.properties") // |
| .getProperty(VERSION_KEY, "Unknown version"); |
| } catch (final Exception ignored) { |
| return "Unknown version(err)"; |
| } |
| } |
| |
| static final class RpcConnectionObserver implements RpcClient.ConnectionObserver { |
| |
| static final Counter CONN_COUNTER = MetricsUtil.counter("connection_counter"); |
| static final Meter CONN_FAILURES = MetricsUtil.meter("connection_failures"); |
| |
| @Override |
| public void onReady(final Endpoint ep) { |
| CONN_COUNTER.inc(); |
| MetricsUtil.counter("connection_counter", ep).inc(); |
| } |
| |
| @Override |
| public void onFailure(final Endpoint ep) { |
| CONN_COUNTER.dec(); |
| CONN_FAILURES.mark(); |
| MetricsUtil.counter("connection_counter", ep).dec(); |
| MetricsUtil.meter("connection_failures", ep).mark(); |
| } |
| |
| @Override |
| public void onShutdown(final Endpoint ep) { |
| CONN_COUNTER.dec(); |
| MetricsUtil.counter("connection_counter", ep).dec(); |
| } |
| } |
| |
| /** |
| * A printer use logger, the {@link #print(Object)} writes data to |
| * an inner buffer, the {@link #println(Object)} actually writes |
| * data to the logger, so we must call {@link #println(Object)} |
| * on the last writing. |
| */ |
| static final class LogPrinter implements Display.Printer { |
| |
| private static final int MAX_BUF_SIZE = 1024 << 3; |
| |
| private final Logger logger; |
| |
| private StringBuilder buf = new StringBuilder(); |
| |
| LogPrinter(Logger logger) { |
| this.logger = logger; |
| } |
| |
| @Override |
| public synchronized Printer print(final Object x) { |
| this.buf.append(x); |
| return this; |
| } |
| |
| @Override |
| public synchronized Printer println(final Object x) { |
| this.buf.append(x); |
| this.logger.info(this.buf.toString()); |
| truncateBuf(); |
| this.buf.setLength(0); |
| return this; |
| } |
| |
| private void truncateBuf() { |
| if (this.buf.capacity() < MAX_BUF_SIZE) { |
| this.buf.setLength(0); // reuse |
| } else { |
| this.buf = new StringBuilder(); |
| } |
| } |
| } |
| } |