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

com.aliyun.odps.tunnel.impl.UpsertSessionImpl Maven / Gradle / Ivy

package com.aliyun.odps.tunnel.impl;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.aliyun.odps.Column;
import com.aliyun.odps.Odps;
import com.aliyun.odps.OdpsType;
import com.aliyun.odps.PartitionSpec;
import com.aliyun.odps.TableSchema;
import com.aliyun.odps.commons.transport.Headers;
import com.aliyun.odps.commons.transport.HttpStatus;
import com.aliyun.odps.commons.transport.Request;
import com.aliyun.odps.data.Record;
import com.aliyun.odps.tunnel.Configuration;
import com.aliyun.odps.tunnel.HttpHeaders;
import com.aliyun.odps.tunnel.TableTunnel;
import com.aliyun.odps.tunnel.TunnelConstants;
import com.aliyun.odps.tunnel.TunnelException;
import com.aliyun.odps.tunnel.TunnelTableSchema;
import com.aliyun.odps.tunnel.io.CompressOption;
import com.aliyun.odps.tunnel.io.TunnelRetryHandler;
import com.aliyun.odps.tunnel.streams.UpsertStream;
import com.aliyun.odps.type.TypeInfoFactory;
import com.aliyun.odps.utils.FixedNettyChannelPool;
import com.aliyun.odps.utils.StringUtils;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;

public class UpsertSessionImpl extends SessionBase implements TableTunnel.UpsertSession {
    private long slotNum;
    private String status;
    private Map buckets = new HashMap<>();
    private String hasher;
    private List hashKeys = new ArrayList<>();
    private TunnelTableSchema recordSchema;
    private long commitTimeout;
    private long connectTimeout;
    private long readTimeout;
    private boolean supportPartialUpdate = false;
    private long lifecycle = 0;

    // netty
    private EventLoopGroup group;
    private Bootstrap bootstrap;

    /**
     * scheduler to reload session periodically, which keep session alive.
     */
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
    private FixedNettyChannelPool channelPool;

    public UpsertSessionImpl(UpsertSessionImpl.Builder builder) throws TunnelException, IOException {
        this.projectName = builder.getProjectName();
        this.schemaName = builder.getSchemaName();
        this.tableName = builder.getTableName();
        this.partitionSpec = builder.getPartitionSpec();
        this.config = builder.getConfig();
        this.httpClient = config.newRestClient(projectName);
        this.slotNum = builder.getSlotNum();
        this.commitTimeout = builder.getCommitTimeout();
        this.connectTimeout = config.getSocketConnectTimeout() * 1000L;
        this.readTimeout = config.getSocketTimeout() * 1000L;
        this.id = builder.getUpsertId();
        this.tunnelRetryHandler = new TunnelRetryHandler(config);
        this.lifecycle = builder.getLifecycle();
        if (id == null) {
            initiate();
        } else {
            reload(true);
        }
        initScheduler();
        if (builder.bootstrap == null) {
            initNettyBootstrap(builder.threadNum);
        } else {
            this.bootstrap = builder.bootstrap;
        }
        channelPool = newChannelPool(builder.concurrentNum);
    }


    public FixedNettyChannelPool getChannelPool() {
        return channelPool;
    }

    @Override
    public Record newRecord() {
        return new UpsertRecord(this.recordSchema.getColumns().toArray(new Column[0]));
    }

    @Override
    public UpsertStream.Builder buildUpsertStream() {
        return new UpsertStreamImpl.Builder().setSession(this)
            .setCompressOption(config.getCompressOption()).setListener(
                new DefaultUpsertSteamListener(this));
    }

    public static class DefaultUpsertSteamListener implements UpsertStream.Listener {
        UpsertSessionImpl session;

        public DefaultUpsertSteamListener(UpsertSessionImpl session) {
            this.session = session;
        }
        @Override
        public void onFlush(UpsertStream.FlushResult result) {
            // do nothing
        }

        @Override
        public boolean onFlushFail(Exception error, int retry) {
            if (error instanceof TunnelException) {
                Integer errorStatus = ((TunnelException) error).getStatus();
                if (errorStatus != null &&
                    (errorStatus == HttpStatus.BAD_GATEWAY
                     || errorStatus == HttpStatus.GATEWAY_TIMEOUT)) {
                    try {
                        session.reload(false);
                    } catch (TunnelException e) {
                        return false;
                    }
                }
            }
            return session.getTunnelRetryHandler().onFailure(error, retry);
        }
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public String getQuotaName() {
        return quotaName;
    }

    @Override
    public String getStatus() throws TunnelException {
        reload(false);
        try {
            readLock.lock();
            return status;
        } finally {
            readLock.unlock();
        }
    }

    @Override
    public TableSchema getSchema() {
        return schema;
    }

    public boolean supportPartialUpdate() {
        return supportPartialUpdate;
    }

    public void commit(boolean async) throws TunnelException {
        HashMap params = getCommonParams();
        params.put(TunnelConstants.UPSERT_ID, id);

        HashMap headers = getCommonHeaders();
        headers.put(HttpHeaders.HEADER_ODPS_ROUTED_SERVER, buckets.get(0).getServer());

        HttpResult result = httpRequest(headers, params, "POST", "commit upsert session");

        load(result, false);

        if (!async) {
            int i = 1;
            long start = System.currentTimeMillis();
            while (status.equalsIgnoreCase(TunnelConstants.SESSION_STATUS_COMMITTING) ||
                   status.equalsIgnoreCase(TunnelConstants.SESSION_STATUS_NORMAL)) {
                try {
                    if (System.currentTimeMillis() - start > commitTimeout) {
                        throw new TunnelException("Commit session timeout");
                    }
                    Thread.sleep(i * 1000);
                    result = httpRequest(headers, params, "POST", "commit upsert session");
                    load(result, false);
                    if (i < 16) {
                        i = i * 2;
                    }
                } catch (InterruptedException e) {
                    throw new TunnelException(e.getMessage(), e);
                } catch (TunnelException e) {
                    if (e.getErrorCode().equalsIgnoreCase("UpsertSessionNotFound")) {
                        status = TunnelConstants.SESSION_STATUS_COMMITTED;
                    } else {
                        throw e;
                    }
                }
            }
            if (!status.equalsIgnoreCase(TunnelConstants.SESSION_STATUS_COMMITTED)) {
                throw new TunnelException("Commit session failed, status:" + status);
            }
        }
    }

    public void abort() throws TunnelException {
        HashMap params = getCommonParams();
        params.put(TunnelConstants.UPSERT_ID, id);

        HashMap headers = getCommonHeaders();
        headers.put(HttpHeaders.HEADER_ODPS_ROUTED_SERVER, buckets.get(0).getServer());

        httpRequest(headers, params, "DELETE", "abort upsert session");
    }

    @Override
    public void close() {
        scheduler.shutdownNow();
        if (group != null) {
            group.shutdownGracefully().syncUninterruptibly();
        }
    }

    private void reload(boolean init) throws TunnelException {
        HashMap params = getCommonParams();
        params.put(TunnelConstants.UPSERT_ID, id);

        HashMap headers = getCommonHeaders();

        HttpResult result = httpRequest(headers, params, "GET", "get upsert session");

        load(result, init);
    }

    private void initiate() throws TunnelException, IOException {

        HashMap params = getCommonParams();

        params.put(TunnelConstants.SLOT_NUM, String.valueOf(this.slotNum));

        if (lifecycle > 0 && lifecycle <=24) {
            params.put(TunnelConstants.LIFECYCLE, String.valueOf(this.lifecycle));
        }

        HashMap headers = getCommonHeaders();

        HttpResult result = httpRequest(headers, params, "POST", "create upsert session");

        load(result, true);
    }

    private void load(HttpResult result, boolean loadAll) throws TunnelException {
        try {
            JsonObject tree = new JsonParser().parse(result.body).getAsJsonObject();
            loadFromJson(result.requestId, tree, loadAll);
        } catch (JsonSyntaxException e) {
            throw new TunnelException(result.requestId, "Invalid json content: '" + result.body + "'", e);
        }
    }

    private void loadFromJson(String requestId, JsonObject tree, boolean loadAll) throws TunnelException {
        try {
            if (tree.has("id") &&
                tree.has("schema") &&
                tree.has("hash_key") &&
                tree.has("hasher") &&
                tree.has("slots") &&
                tree.has("status")) {
                if (loadAll) {
                    // session id
                    id = tree.get("id").getAsString();
                    // schema
                    JsonObject tunnelTableSchema = tree.get("schema").getAsJsonObject();
                    schema = new TunnelTableSchema(tunnelTableSchema);
                    this.recordSchema = new TunnelTableSchema(tunnelTableSchema);
                    this.recordSchema.addColumn(
                            new Column(TunnelConstants.META_FIELD_VERSION, TypeInfoFactory.getPrimitiveTypeInfo(OdpsType.BIGINT)));
                    this.recordSchema.addColumn(
                            new Column(TunnelConstants.META_FIELD_APP_VERSION, TypeInfoFactory.getPrimitiveTypeInfo(OdpsType.BIGINT)));
                    this.recordSchema.addColumn(
                            new Column(TunnelConstants.META_FIELD_OPERATION, TypeInfoFactory.getPrimitiveTypeInfo(OdpsType.TINYINT)));
                    this.recordSchema.addColumn(new Column(TunnelConstants.META_FIELD_KEY_COLS, TypeInfoFactory
                            .getArrayTypeInfo(TypeInfoFactory.getPrimitiveTypeInfo(OdpsType.BIGINT))));
                    this.recordSchema.addColumn(new Column(TunnelConstants.META_FIELD_VALUE_COLS, TypeInfoFactory
                            .getArrayTypeInfo(TypeInfoFactory.getPrimitiveTypeInfo(OdpsType.BIGINT))));
                    // hash key
                    tree.get("hash_key").getAsJsonArray().forEach(v -> this.hashKeys.add(v.getAsString()));
                    // has function
                    this.hasher = tree.get("hasher").getAsString();

                    if (tree.has("quota_name")) {
                        quotaName = tree.get("quota_name").getAsString();
                    }
                    if (tree.has(TunnelConstants.ENABLE_PARTIAL_UPDATE)) {
                        supportPartialUpdate = tree.get(TunnelConstants.ENABLE_PARTIAL_UPDATE).getAsBoolean();
                    }
                }
            } else {
                throw new TunnelException(requestId, "Incomplete session info: '" + tree.toString() + "'");
            }

            // slots
            Map bucketMap = new HashMap<>();
            List slotList = new ArrayList<>();
            JsonArray slotElements = tree.getAsJsonArray("slots");
            for (JsonElement slotElement : slotElements) {
                JsonObject slotInfo = slotElement.getAsJsonObject();
                String slotId = slotInfo.get("slot_id").getAsString();
                JsonArray bucketElements = slotInfo.get("buckets").getAsJsonArray();
                String workerAddr = slotInfo.get("worker_addr").getAsString();
                Slot slot = new Slot(slotId, workerAddr);
                slotList.add(slot);
                for (JsonElement bucketElement : bucketElements) {
                    bucketMap.put(bucketElement.getAsInt(), slot);
                }
            }

            for (Integer bucket : bucketMap.keySet()) {
                if (bucket < 0 || bucket >= bucketMap.size()) {
                    throw new TunnelException("Invalid bucket value:" + bucket);
                }
            }

            try {
                writeLock.lock();
                this.buckets = bucketMap;
                this.status = tree.get("status").getAsString();
            } finally {
                writeLock.unlock();
            }
        } catch (TunnelException e) {
            throw e;
        } catch (Exception e) {
            throw new TunnelException(requestId, "Invalid json content: '" + tree.toString() + "'", e);
        }
    }

  private void initScheduler() {
    final Runnable task = () -> {
      try {
        reload(false);
        if (!status.equalsIgnoreCase(TunnelConstants.SESSION_STATUS_NORMAL) &&
            !status.equalsIgnoreCase(TunnelConstants.SESSION_STATUS_COMMITTING)) {
          scheduler.shutdownNow();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    };
    scheduler.scheduleAtFixedRate(task, 30L, 30L, TimeUnit.SECONDS);
  }

    /**
     * 用于初始化http相关成员变量,包括netty的线程组和通用的header与parameter
     */
    private void initNettyBootstrap(int threadNum) throws TunnelException {
        group = new NioEventLoopGroup(threadNum);
        bootstrap = generateNettyBootstrap(config, group);
    }

    private FixedNettyChannelPool newChannelPool(int concurrentNum) throws TunnelException {
        try {
            URI uri = new URI(httpClient.getEndpoint() + getResource());
            String host = uri.getHost();
            int port = uri.getPort();
            if (port == -1) {
                if ("https".equalsIgnoreCase(uri.getScheme())) {
                    port = 443;
                } else {
                    port = 80;
                }
            }
            int finalPort = port;
            FixedNettyChannelPool.ChannelFactory channelFactory =
                () -> bootstrap.connect(host, finalPort).sync().channel();
            return new FixedNettyChannelPool(concurrentNum, channelFactory);
        } catch (Exception e) {
            throw new TunnelException(e.getMessage(), e);
        }
    }

    public static Bootstrap generateNettyBootstrap(Configuration configuration,
                                                   EventLoopGroup group) {
        Bootstrap bootstrap = new Bootstrap();
        Odps odps = configuration.getOdps();
        bootstrap.group(group).channel(NioSocketChannel.class)
            .handler(new ChannelInitializer() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    URI uri = new URI(odps.getEndpoint());
                    if ("https".equalsIgnoreCase(uri.getScheme())) {
                        SslContextBuilder builder = SslContextBuilder.forClient();
                        if (odps.getRestClient().isIgnoreCerts()) {
                            builder = builder.trustManager(InsecureTrustManagerFactory.INSTANCE);
                        }
                        SslContext sc = builder.build();
                        channel.pipeline().addLast(sc.newHandler(channel.alloc()));
                    }
                    channel.pipeline().addLast(new HttpClientCodec())
                        .addLast(new HttpObjectAggregator(65536))
                        .addLast(new HttpContentDecompressor());
                }
            });
        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
                         configuration.getSocketConnectTimeout() * 1000);
        return bootstrap;
    }

    void updateBuckets(int bucketId, String newSlotServer) throws TunnelException {
        if (StringUtils.isNullOrEmpty(newSlotServer)) {
            reload(false);
        } else {
            try {
                readLock.lock();
                buckets.get(bucketId).setServer(newSlotServer);
            } finally {
                readLock.unlock();
            }
        }
    }

    Request buildRequest(String method,
                                int bucket,
                                Slot slot,
                                long contentLength,
                                long recordCount,
                                CompressOption compressOption) throws TunnelException {
        if (slot.getServer().isEmpty()) {
            throw new TunnelException("slot addr is empty");
        }
        HashMap params = getCommonParams();
        params.put(TunnelConstants.BUCKET_ID, String.valueOf(bucket));
        params.put(TunnelConstants.SLOT_ID, slot.getSlot());
        params.put(TunnelConstants.UPSERT_ID, id);

        HashMap headers = Util.getCommonHeader();
        headers.put(Headers.CONTENT_LENGTH, String.valueOf(contentLength));
        headers.put(HttpHeaders.HEADER_ODPS_ROUTED_SERVER, slot.getServer());
        List tags = config.getTags();
        if (tags != null) {
            headers.put(HttpHeaders.HEADER_ODPS_TUNNEL_TAGS, String.join(",", tags));
        }
        params.put(TunnelConstants.RECORD_COUNT, String.valueOf(recordCount));

        switch (compressOption.algorithm) {
            case ODPS_RAW: {
                break;
            }
            case ODPS_ZLIB: {
                headers.put(Headers.CONTENT_ENCODING, "deflate");
                break;
            }
            case ODPS_SNAPPY: {
                headers.put(Headers.CONTENT_ENCODING, "x-snappy-framed");
                break;
            }
            case ODPS_LZ4_FRAME: {
                headers.put(Headers.CONTENT_ENCODING, "x-lz4-frame");
                break;
            }
            default: {
                throw new TunnelException("unsupported compression option.");
            }
        }

        return httpClient.buildRequest(getResource(), method, params, headers);
    }

    URI getEndpoint() throws TunnelException {
        return config.getEndpoint(projectName);
    }

    Map getBuckets() {
        try {
            readLock.lock();
            return buckets;
        } finally {
            readLock.unlock();
        }
    }

    TunnelTableSchema getRecordSchema() {
        return recordSchema;
    }

    String getHasher() {
        return hasher;
    }

    List getHashKeys() {
        List keys = new ArrayList<>();
        for (String key : hashKeys) {
            keys.add(recordSchema.getColumnIndex(key));
        }
        return keys;
    }

    Bootstrap getBootstrap () {
        return bootstrap;
    }

    protected String getResource() {
        return config.getResource(projectName, schemaName, tableName)+ "/" + TunnelConstants.UPSERTS;
    }

    public long getConnectTimeout() {
        return connectTimeout;
    }

    public long getReadTimeout() {
        return readTimeout;
    }

    public static class Builder implements TableTunnel.UpsertSession.Builder {
        private String upsertId;
        private String projectName;
        private String schemaName;
        private String tableName;
        private PartitionSpec partitionSpec;
        Bootstrap bootstrap;
        int concurrentNum = 20;
        int threadNum = 1;
        private long slotNum = 1;
        private long commitTimeout = 120 * 1000;
        private long lifecycle = 0;
        Configuration config;

        public String getUpsertId() {
            return upsertId;
        }

        public UpsertSessionImpl.Builder setUpsertId(String upsertId) {
            this.upsertId = upsertId;
            return this;
        }

        public String getProjectName() {
            return projectName;
        }

        public UpsertSessionImpl.Builder setProjectName(String projectName) {
            this.projectName = projectName;
            return this;
        }

        public String getSchemaName() {
            return schemaName;
        }

        public UpsertSessionImpl.Builder setSchemaName(String schemaName) {
            this.schemaName = schemaName;
            return this;
        }

        public String getTableName() {
            return tableName;
        }

        public UpsertSessionImpl.Builder setTableName(String tableName) {
            this.tableName = tableName;
            return this;
        }

        public String getPartitionSpec() {
            return this.partitionSpec == null ? null : partitionSpec.toString().replaceAll("'", "");
        }

        public UpsertSessionImpl.Builder setPartitionSpec(PartitionSpec spec) {
            this.partitionSpec = spec;
            return this;
        }

        public UpsertSessionImpl.Builder setPartitionSpec(String spec) {
            this.partitionSpec = spec == null ? null : new PartitionSpec(spec);
            return this;
        }

        public long getSlotNum() {
            return slotNum;
        }

        public UpsertSessionImpl.Builder setSlotNum(long slotNum) {
            this.slotNum = slotNum;
            return this;
        }

        public Configuration getConfig() {
            return config;
        }

        public UpsertSessionImpl.Builder setConfig(Configuration config) {
            if (config == null) {
                throw new IllegalArgumentException("config can not be null!");
            }
            this.config = config;
            return this;
        }

        public long getCommitTimeout() {
            return commitTimeout;
        }

        public TableTunnel.UpsertSession.Builder setCommitTimeout(long commitTimeout) {
            if (commitTimeout <= 0) {
                throw new IllegalArgumentException("timeout value must be positive");
            }
            this.commitTimeout = commitTimeout;
            return this;
        }

        @Override
        public UpsertSessionImpl.Builder setNetworkThreadNum(int threadNum) {
            this.threadNum = threadNum;
            return this;
        }

        @Override
        public UpsertSessionImpl.Builder setConcurrentNum(int concurrentNum) {
            this.concurrentNum = concurrentNum;
            return this;
        }

        @Override
        public UpsertSessionImpl.Builder setConnectTimeout(long timeout) {
            if (timeout <= 0) {
                throw new IllegalArgumentException("timeout value must be positive");
            }
            config.setSocketConnectTimeout((int) (timeout / 1000));
            return this;
        }

        @Override
        public UpsertSessionImpl.Builder setReadTimeout(long timeout) {
            if (timeout <= 0) {
                throw new IllegalArgumentException("timeout value must be positive");
            }
            config.setSocketTimeout((int) (timeout / 1000));
            return this;
        }

        public long getLifecycle() {
            return lifecycle;
        }

        public UpsertSessionImpl.Builder setLifecycle(long lifecycle) {
            this.lifecycle = lifecycle;
            return this;
        }

        public UpsertSessionImpl.Builder setNettyBootStrap(Bootstrap bootstrap) {
            this.bootstrap = bootstrap;
            return this;
        }

        public UpsertSessionImpl build() throws TunnelException, IOException {
            return new UpsertSessionImpl(this);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy