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

net.snowflake.client.IngestFilesTester Maven / Gradle / Ivy

/*
 * Copyright (c) 2012, 2013 Snowflake Computing Inc. All right reserved.
 */

package net.snowflake.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.google.common.net.MediaType;
import net.snowflake.client.core.SFSession;
import net.snowflake.client.ingest.IngestHistoryResponse;
import net.snowflake.client.ingest.avro.Row;
import org.apache.avro.file.DataFileWriter;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.specific.SpecificDatumWriter;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.lang.JoseException;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;

public class IngestFilesTester
{

  private static final String JSON_UTF8_CT = MediaType.JSON_UTF_8.toString();

  public enum FileFormat
  {
    CSV("csv"),
    JSON("json"),
    AVRO("avro"),
    XML("xml");

    final private String formatNameString;

    FileFormat(String formatString)
    {
      this.formatNameString = formatString;
    }

    public String getFormatName()
    {
      return this.formatNameString;
    }
  }

  protected final String RND_PAD =
      Math.abs(ThreadLocalRandom.current().nextLong()) +
          "" + System.currentTimeMillis();

  protected final String SCHEMA = "testschema".toUpperCase() + RND_PAD;
  protected final String DATABASE = "testdb".toUpperCase() + RND_PAD;

  protected final String INGEST_TBL = "ing_t";
  protected final String PIPE_NAME = "ingest_pipe";
  protected final String STAGE_NAME = "ingest_stage";

  protected final String fqTableName =
      DATABASE + "." + SCHEMA + "." + quote(INGEST_TBL);

  protected final String fqPipeName =
      DATABASE + "." + SCHEMA + "." + quote(PIPE_NAME);


  protected final Connection connection;
  protected final CloseableHttpClient httpClient;
  protected final Path tmpIngestStage = createTempStageDir();
  protected final ObjectMapper objectMapper;

  private final ExecutorService executorService =
      Executors.newCachedThreadPool();


  protected Path createTempStageDir()
  {
    final String base = "/tmp/ingest_stage";
    final Path basedir = Paths.get(base);
    try
    {
      Files.createDirectories(Paths.get(base));
      final String prefix = String.valueOf(RND_PAD + "_");
      return Files.createTempDirectory(basedir, prefix);
    }
    catch (IOException e)
    {
      throw new IllegalStateException("create temp dir failed", e);
    }
  }


  protected IngestFilesTester(Connection connection,
                            CloseableHttpClient httpClient)
  {
    this.connection = connection;
    this.httpClient = httpClient;
    objectMapper = new ObjectMapper();
  }

  public static CloseableHttpClient createHttpClient()
  {
    return HttpClientBuilder.create().build();
  }

  public static void main(String[] args)
      throws Exception
  {

    final int inter_batch_delay = 250;
    final int history_poll_interval = 3000;
    final int batch_size = 1;
    final int max_rows_per_file = 5;
    final int push_threads = 2;
    final int batch_count = push_threads * 240;

    fullTest(inter_batch_delay, history_poll_interval, batch_size,
             max_rows_per_file, push_threads, batch_count, true);

    say("System.exit(0)");
    System.exit(0);
  }


  static boolean fullTest(int inter_batch_delay,
                          int history_poll_interval,
                          int batch_size,
                          int max_rows_per_file,
                          int push_threads,
                          int batch_count,
                          boolean encrypt_fdn_files)
      throws Exception
  {

    boolean success = false;

    enableIngestService();
    enableIngestForAccount(true);

    //TODO: stop making this global
    enableKeyPairAuthn();

    try (Connection connection = getConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);
      success =
          test.doTest(FileFormat.CSV, inter_batch_delay, history_poll_interval,
                      batch_size, max_rows_per_file, push_threads, batch_count,
                      encrypt_fdn_files);
    }

    //TODO: we need to test the failure cases!!

    say("test complete");

    return success;
  }

  protected static final class SecurityState implements AutoCloseable
  {
    private final KeyPair pair;
    private final KeyFactory keyFactory;
    private final String jwtToken;

    private static final String ALGORITHM = "RSA";

    SecurityState(String account, String user, KeyPair pair)
        throws NoSuchAlgorithmException
    {
      this.pair = Preconditions.checkNotNull(pair);
      keyFactory = KeyFactory.getInstance(ALGORITHM);
      jwtToken = generateJwtToken(account, user, pair);
    }

    public static SecurityState create(String account, String user)
        throws NoSuchAlgorithmException, NoSuchProviderException
    {
      KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
      SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
      keyGen.initialize(2048, random);
      KeyPair pair = keyGen.generateKeyPair();
      return new SecurityState(account, user, pair);
    }

    @Override
    public void close() throws Exception
    {
    }

    public String getPublicKeyString() throws InvalidKeySpecException
    {
      final PublicKey pk = pair.getPublic();
      X509EncodedKeySpec spec =
          keyFactory.getKeySpec(pk, X509EncodedKeySpec.class);
      return Base64.encodeBase64String(spec.getEncoded());
    }

    public String getJwtToken()
    {
      return jwtToken;
    }

    public static String generateJwtToken(String account,
                                           String user,
                                           KeyPair pair)
    {
      // Create the Claims, which will be the content of the JWT
      JwtClaims claims = new JwtClaims();

      //seems we need upper case...
      final String issuer = (account + "." + user).toUpperCase();

      claims.setIssuer(issuer);  // who creates the token and signs it

      //snowflake will expire token in 1 hour anyway.
      final int ttl = 59;

      //time when the token will expire
      claims.setExpirationTimeMinutesInTheFuture(ttl);

      //when the token was issued/created (now)
      claims.setIssuedAtToNow();

      // A JWT is a JWS and/or a JWE with JSON claims as the payload.
      // In this case it is a JWS so we create a JsonWebSignature object.
      JsonWebSignature jws = new JsonWebSignature();

      // The payload of the JWS is JSON content of the JWT Claims
      final String json = claims.toJson();
      //System.out.println("**** json = " + json);
      jws.setPayload(json);

      // The JWT is signed using the private key
      final Key key = pair.getPrivate();
      jws.setKey(key);

      // Set the signature algorithm on the JWT/JWS that
      // will integrity protect the claims
      jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);

      // Sign the JWS and produce the compact serialization or the complete
      // JWT/JWS representation, which is a string consisting
      // of three dot ('.') separated base64url-encoded parts
      // in the form Header.Payload.Signature
      String jwt;
      try
      {
        jwt = jws.getCompactSerialization();
      }
      catch (JoseException e)
      {
        throw new RuntimeException("failed to generate JWT token", e);
      }

      //System.out.println("@@@@@@ jwt = " + jwt);


      return jwt;
    }
  }

  public static void enableIngestServiceOld() throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);
      test.doQuery("ALTER system SET INGEST_ACTIVE_CLUSTER=\"default\"");
    }
  }

  public static void enableIngestService() throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);

      final Set gs_ids = getGsIds(test);

      final String ids = quote(Joiner.on(',').join(gs_ids));
      test.doQuery("alter system set INGEST_ACTIVE_INSTANCES_OVERRIDE=" + ids);
    }
  }

  //NOTE: this does not completely disable ingest service, just makes
  //it so that no nodes will work on ingest queues.
  public static void disableIngestService() throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);

      test.doQuery("alter system set INGEST_ACTIVE_INSTANCES_OVERRIDE=\"\"");
    }
  }

  private static Set getGsIds(IngestFilesTester tester) throws SQLException
  {
    final Set gs_ids = new HashSet<>();
    try (Statement statement = tester.connection.createStatement())
    {
      try (ResultSet resultSet = statement.executeQuery("show gsinstances"))
      {
        while (resultSet.next())
        {
          final long gs_id = resultSet.getLong(1);
          gs_ids.add(gs_id);
        }
      }
    }
    return gs_ids;
  }

  public static void enableKeyPairAuthn() throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);
      test.doQuery("ALTER system SET ENABLE_KEY_PAIR_AUTH=true");
    }
  }

  protected static void enableEncryption(boolean enabled)
      throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);
      test.doQuery("alter account " + TestConnectionUtil.ACCOUNT +
                       " set encrypt_data = " + enabled);
    }
  }

  public static void enableIngestForAccount(boolean enabled)
      throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);
      test.doQuery("alter account " + TestConnectionUtil.ACCOUNT +
                       " set ENABLE_INGEST_FEATURE = " + enabled);
    }
  }

  public static void disableIngestServiceOld() throws SQLException, IOException
  {
    try (Connection connection = getAdminConnection();
        CloseableHttpClient httpClient = createHttpClient())
    {
      final IngestFilesTester test =
          new IngestFilesTester(connection, httpClient);
      test.doQuery("ALTER system SET INGEST_ACTIVE_CLUSTER=default");
    }
  }

  private boolean doTest(FileFormat fileFormat,
                         int inter_batch_delay,
                         int history_poll_interval,
                         int batch_size,
                         int max_rows_per_file,
                         int push_threads,
                         int batch_count,
                         boolean encrypt_fdn_files)
      throws Exception
  {
    doQuery("create database " + DATABASE);
    doQuery("use database " + DATABASE);
    doQuery("create schema " + SCHEMA);
    doQuery("use schema " + SCHEMA);

    final String insert_role = "API_INSERT";
    doQuery("use role accountadmin");
    doQuery("create or replace role " + insert_role);


    final String user = "user" + RND_PAD;
    doQuery("create user " + user + " default_role = " + insert_role +
                " password=\"" + TestConnectionUtil.PASSWORD + "\"");
    doQuery("grant role " + insert_role + " to user " + user);


    final String account = TestConnectionUtil.ACCOUNT;

    say("begin setup security");
    final SecurityState securityState = SecurityState.create(account, user);
    say("end setup security");

    String pk = securityState.getPublicKeyString();
    assert pk != null;
    assert !pk.isEmpty();

    doQuery("alter user " + user +
                " set RSA_PUBLIC_KEY='" + pk + "'");


    final String truthTable = quote(INGEST_TBL + "_TRUTH");


//    final SessionUtil.LoginOutput loginOutput = login();
//    final String sessionToken = loginOutput.getSessionToken();

    setupTables(fileFormat, truthTable);



    doQuery("grant INSERT on " + quote(INGEST_TBL) + " to ROLE " + insert_role);

    //for read history.
    doQuery("grant SELECT on " + quote(INGEST_TBL) + " to ROLE " + insert_role);

    doQuery("grant USAGE on STAGE " + quote(STAGE_NAME)
                + " to ROLE " + insert_role);

    //TODO: avoid need for grant ALL
    doQuery("grant ALL on DATABASE " + DATABASE +
                " to ROLE " + insert_role);
    doQuery("grant ALL on SCHEMA " + SCHEMA +
                " to ROLE " + insert_role);

//    doQuery("grant USAGE on DATABASE " + DATABASE +
//                " to ROLE " + insert_role);
//    doQuery("grant USAGE on SCHEMA " + SCHEMA +
//                " to ROLE " + insert_role);
//

    createPipe(user);

    if (encrypt_fdn_files) {
      enableEncryption(true);
    }

    try
    {
      runTest(fileFormat,
              inter_batch_delay,
              batch_count,
              batch_size,
              max_rows_per_file,
              securityState.getJwtToken(),
              push_threads,
              history_poll_interval);

      return checkWithCopy(truthTable);
    }
    finally
    {
      doQuery("drop database " + DATABASE);
      disableIngestService();

      if (encrypt_fdn_files) {
        //NOTE: assumes default for the account is false!
        enableEncryption(false);
      }

    }
  }

  public void createPipe(String user) throws SQLException
  {
    //creating pipe as the ingest user since
    //we don't have GRANT for PIPE yet.
    try (Connection user_conn =
        TestConnectionUtil.getConnection(user, TestConnectionUtil.ACCOUNT))
    {
      final IngestFilesTester user_tester =
          new IngestFilesTester(user_conn, this.httpClient);

      user_tester.doQuery("use database " + DATABASE);
      user_tester.doQuery("use schema " + SCHEMA);

      final String query =
          "create pipe " + quote(PIPE_NAME) + " as COPY into " +
              quote(INGEST_TBL) + " from @" + quote(STAGE_NAME);
      user_tester.doQuery(query);
    }
  }

  private void runTest(final FileFormat fileFormat,
                       final int inter_batch_delay,
                       final int batch_count,
                       final int batch_size,
                       final int max_rows_per_file,
                       final String jwtToken,
                       final int push_threads,
                       final int history_poll_interval)
      throws IOException, URISyntaxException, InterruptedException,
             ExecutionException
  {

    final ConcurrentHashMap pushed = new ConcurrentHashMap<>();
    final ConcurrentHashMap seen = new ConcurrentHashMap<>();
    final AtomicInteger batchesRemaining = new AtomicInteger(batch_count);

    Callable pusher = new Callable()
    {
      @Override
      public Integer call() throws Exception
      {
        final ThreadLocalRandom rnd = ThreadLocalRandom.current();
        int cnt = 0;
        while (batchesRemaining.decrementAndGet() > 0)
        {

          final Collection files =
              createTempFiles(batch_size, max_rows_per_file, fileFormat);


          final Boolean success = postFiles(files, jwtToken);
          if (success)
          {
            cnt += files.size();
            for (Path file : files)
            {
              final String f = file.getFileName().toString();
              pushed.put(f, f);
            }
          }
          else
          {
            throw new IllegalStateException("post failed");
          }

          //and random amout up to 10% to desync threads...
          final int sl = inter_batch_delay +
              rnd.nextInt(1 + inter_batch_delay / 10);
          Thread.sleep(sl);
        }
        return cnt;
      }
    };

    List> pusher_futures = new ArrayList<>();
    for (int i = 0; i < push_threads; i++)
    {
      final Future f = executorService.submit(pusher);
      pusher_futures.add(f);
    }


    Callable watcher = new Callable()
    {
      @Override
      public Integer call() throws Exception
      {
        while (batchesRemaining.get() > 0 || seen.size() < pushed.size())
        {
          try
          {
            final Collection files_seen = getHistory(jwtToken);
            say("saw " + files_seen.size() + " files from history");
            for (String f : files_seen)
            {
              final URI file_uri = URI.create(f);
              String p = file_uri.getPath();
              if (p.startsWith("/"))
              {
                p = p.substring(1);
              }
              seen.put(p, p);
            }
          }
          catch (Exception e)
          {
            //our test might involve restarting GS so we want to just keep
            //trying...
            say("got error fetching history: " + e);
          }

          Thread.sleep(history_poll_interval);

          final Sets.SetView missing =
              Sets.difference(pushed.keySet(), seen.keySet());
          say("## waiting on " + missing.size() + " files: " + missing);
        }

        return 0; //unused for now.
      }
    };

    final Future watcher_future = executorService.submit(watcher);


    //we call get() on futures to surface any exceptions.

    for (Future pusher_future : pusher_futures)
    {
      final Integer cnt = pusher_future.get();
    }

    say("%%%%%% pushers done. file cnt = " + pushed.size());

    final Integer ign = watcher_future.get();

    if (!seen.equals(pushed))
    {
      say("pushed " + pushed);
      say("seen   " + seen);
      throw new IllegalStateException("mismatch");
    }

    say(pushed.size() + " files ingested");
  }

  //returns true if check was a success
  public boolean checkWithCopy(String truthTable) throws SQLException
  {
    doQuery("copy into " + truthTable + " from @" + quote(STAGE_NAME));

    // Check if tha ingest table has the same rows as the truth table. If it
    // does then cnt should be 0
    int cnt =
        doQuery(
            "(select * from " + quote(INGEST_TBL) + " except select * from " +
                truthTable + ") UNION ALL (select * from " + truthTable +
                " except select * from " + quote(INGEST_TBL) + ")");

    say("number of different rows between the " +
            "truth table and the ingest table: " + cnt);


    if (cnt == 0)
    {
      say("*** SUCCESS ***  no diff");
    }
    else
    {
      say("!!! FAILURE !!! " + cnt + " diffs found");
      doQuery("select count(*) from " + truthTable);
      doQuery("select count(*) from " + quote(INGEST_TBL));
    }
    return cnt == 0;
  }

  private void setupTables(FileFormat fileFormat,
                           String truthTable) throws SQLException
  {
    switch (fileFormat)
    {
      case JSON:
        doQuery("create or replace stage " + quote(STAGE_NAME) +
                    " url='file://" + tmpIngestStage + "' " +
                    " file_format=(type=json)");
        doQuery("create or replace table " + quote(INGEST_TBL) +
                    " (v variant)");
        doQuery("create or replace table " + truthTable +
                    " (v variant)");
        break;
      case CSV:
      default:
        doQuery("create or replace stage " + quote(STAGE_NAME) +
                    " url='file://" + tmpIngestStage + "'");
        doQuery("create or replace table " + quote(INGEST_TBL) +
                    " (row_id int,row_str string,num int, src string)");
        doQuery("create or replace table " + truthTable +
                    " (row_id int,row_str string,num int, src string)");
    }
  }

  private Collection getHistory(String jwtToken)
      throws URISyntaxException, IOException
  {
    final URI reportEndpoint = historyReportEndpoint();
    HttpGet httpGet = new HttpGet(reportEndpoint);
    addAuthnHeader(httpGet, jwtToken);

    final ResponseHandler> rh =
        new ResponseHandler>()

        {
          @Override
          public Collection handleResponse(HttpResponse response)
              throws IOException
          {
            final StatusLine statusLine = response.getStatusLine();
            final int statusCode = statusLine.getStatusCode();
            if (statusCode == 200)
            {
              final IngestHistoryResponse report =
                  unmarshallHistory(response);

              if (report == null)
              {
                return Collections.emptyList();
              }

              if (!report.completeResult)
              {
                //consider throwing...
                say("INCOMPLETE_REPORT!!");
              }

              final List files1 =
                  report.files;
              Set seen = new HashSet<>();
              for (IngestHistoryResponse.FileEntry entry : files1)
              {
                if (entry.complete)
                {
                  seen.add(entry.path);
                }
              }

              final Map statistics = report.statistics;
              if (statistics != null && !statistics.isEmpty())
              {
                say("report stats: " + statistics);
              }
              return seen;
            }
            else
            {
              final HttpEntity entity = response.getEntity();
              final String json = EntityUtils.toString(entity);
              say("!!! ERROR getting report from " + reportEndpoint +
                      " : " + json);
              return Collections.emptyList();
            }
          }
        };

    return httpClient.execute(httpGet, rh);
  }

  /*
  private IngestHistoryResponse unmarshallHistory(
      HttpResponse response) throws IOException
  {
    //TODO: avoid extra buffering.
    final HttpEntity entity = response.getEntity();
    final Header ct = response.getLastHeader(HttpHeaders.CONTENT_TYPE);
    final MediaType mt = MediaType.parse(ct.getValue());
    final MediaType jsonUtf8 = MediaType.JSON_UTF_8;
    if (mt.type().equals(jsonUtf8.type()) &&
        mt.subtype().equals(jsonUtf8.subtype()))
    {
      final InputStream content = entity.getContent();
      if (content == null)
      {
        throw new IllegalStateException("no content!");
      }
      return objectMapper.readValue(content, IngestHistoryResponse.class);
    }
    else
    {
      throw new IllegalStateException("unknown content type: " + mt);
    }
  }
  */

  private IngestHistoryResponse unmarshallHistory(
      HttpResponse response) throws IOException
  {
    //TODO: avoid extra buffering.
    final HttpEntity entity = response.getEntity();
    final Header ct = response.getLastHeader(HttpHeaders.CONTENT_TYPE);
    final MediaType mt = MediaType.parse(ct.getValue());
    final MediaType jsonUtf8 = MediaType.JSON_UTF_8;
    if (mt.type().equals(jsonUtf8.type()) &&
        mt.subtype().equals(jsonUtf8.subtype()))
    {
      final String json = EntityUtils.toString(entity);
      say("history_response: \n" + json);
      return objectMapper.readValue(json, IngestHistoryResponse.class);
    }
    else
    {
      throw new IllegalStateException("unknown content type: " + mt);
    }
  }

  public Collection createTempFiles(int num_files, int max_rows,
                                           FileFormat fileFormat)
      throws IOException
  {
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();

    List files = new ArrayList<>(num_files);
    for (int i = 0; i < num_files; i++)
    {
      final int row_cnt = 1 + rnd.nextInt(max_rows);
      final Path tempFile;
      switch (fileFormat)
      {
        case JSON:
          tempFile = createTempJson(row_cnt);
          break;
        case XML:
          tempFile = createTempXML(row_cnt);
          break;
        case AVRO:
          tempFile = createTempAVRO(row_cnt);
          break;
        case CSV:
        default:
          tempFile = createTempCsv(row_cnt);
      }
      files.add(tempFile);
    }
    return files;
  }

  private Boolean postFiles(Collection csv_files, String jwt_token)
      throws Exception
  {
    final URI insert_endpoint = insertFilesEndpoint();
    say("pushing " + csv_files.size() + " files to " + insert_endpoint);
    final HttpPost post = new HttpPost(insert_endpoint);

    addAuthnHeader(post, jwt_token);

    final String body = createPostJsonBody(csv_files);

    final StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8);
    entity.setContentType(JSON_UTF8_CT);
    post.setEntity(entity);

    final ResponseHandler handler = new ResponseHandler()
    {
      @Override
      public Boolean handleResponse(HttpResponse response)
          throws IOException
      {
        final StatusLine statusLine = response.getStatusLine();
        final int statusCode = statusLine.getStatusCode();
        final HttpEntity responseEntity = response.getEntity();
        final String str = EntityUtils.toString(responseEntity);
        if (statusCode == 200)
        {
          say("insert returned: " + str);
          return true;
        }
        else
        {
          say("postFiles request failed got: " + statusLine + "\n:" + str);
          return false;
        }
      }
    };

    final int attempts = 60;

    for (int i = 1; i <= attempts; i++)
    {
      try
      {
        final Boolean ok = httpClient.execute(post, handler);
        assert ok != null;
        if (ok)
        {
          return true; //else retry
        }
      }
      catch (Exception e)
      {
        say("got error " + e +
                " during file push on attempt " + i + " of " + attempts);

      }

      Thread.sleep(1000);
    }

    throw new IllegalStateException("giving up on post");
  }

  //body is newline separated filenames
  private static String createPostBody(Collection csv_files)
  {
    StringBuilder sb = new StringBuilder(csv_files.size() * 10);
    for (Path csv_file : csv_files)
    {
      sb.append(csv_file.getFileName().toString());
      sb.append('\n');
    }
    return sb.toString();
  }

  private String createPostJsonBody(Collection csv_files)
      throws IOException
  {
    IngestPushFilesRequest req = new IngestPushFilesRequest();
    final List files =
        new ArrayList<>(csv_files.size());

    for (Path csv_file : csv_files)
    {
      final IngestPushFilesRequest.IngestFile file =
          new IngestPushFilesRequest.IngestFile();
      file.path = csv_file.getFileName().toString();
      file.size = Files.size(csv_file);
      files.add(file);
    }

    req.files = files;

    return this.objectMapper.writeValueAsString(req);
  }

  private static void addAuthnHeader(HttpUriRequest request,
                                     String jwt_token)
  {
    request.setHeader(SFSession.SF_HEADER_AUTHORIZATION,
                      "Bearer " + jwt_token);
  }

  private URI insertFilesEndpoint() throws URISyntaxException
  {
    return insertFilesEndpoint(fqPipeName);
  }

  public static URI insertFilesEndpoint(String fqPipeName) throws URISyntaxException
  {
    URIBuilder bldr = new URIBuilder();
    bldr.setScheme("http");
    bldr.setHost(TestConnectionUtil.HOST);
    bldr.setPort(TestConnectionUtil.PORT);


    bldr.setParameter("requestId", UUID.randomUUID().toString());

    final String path =
        String.format("/v1/data/pipes/%s/insertFiles",
                      fqPipeName);

    bldr.setPath(path);

    return bldr.build();
  }

  protected static String quote(String str)
  {
    return '"' + str + '"';
  }

  private URI historyReportEndpoint() throws URISyntaxException
  {
    return historyReportEndpoint(fqPipeName);
  }

  public static URI historyReportEndpoint(String fqPipeName)
      throws URISyntaxException
  {
    URIBuilder bldr = new URIBuilder();
    bldr.setScheme("http");
    bldr.setHost(TestConnectionUtil.HOST);
    bldr.setPort(TestConnectionUtil.PORT);

    bldr.setParameter("requestId", UUID.randomUUID().toString());

    final String path =
        String.format("/v1/data/pipes/%s/insertReport",
                      fqPipeName);

    bldr.setPath(path);

    return bldr.build();
  }

  private Path createTempJson(int rows) throws IOException
  {
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();

    final Path json = Files.createTempFile(tmpIngestStage, "data", ".json");

    try (Writer w = Files.newBufferedWriter(json, StandardCharsets.UTF_8))
    {
      for (int i = 0; i < rows; i++)
      {
        w.write("{\"row-" + String.valueOf(i) + "\":\""
                    + String.valueOf(rnd.nextLong()) + "\"}");
        w.write("\n");
      }
    }
    return json;
  }

  private Path createTempXML(int rows) throws IOException
  {
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();

    final Path xml = Files.createTempFile(tmpIngestStage, "data", ".xml");

    try (Writer w = Files.newBufferedWriter(xml, StandardCharsets.UTF_8))
    {
      for (int i = 0; i < rows; i++)
      {
        w.write(""
            + String.valueOf(rnd.nextLong()) + "");
        w.write("\n");
      }
    }
    return xml;
  }

  private Path createTempAVRO(int rows) throws IOException
  {
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();

    final Path avro = Files.createTempFile(tmpIngestStage, "data", ".avro");

    DatumWriter rowDatumWriter = new SpecificDatumWriter<>(Row.class);
    DataFileWriter fileWriter = new DataFileWriter<>(rowDatumWriter);

    OutputStream out = Files.newOutputStream(avro);
    fileWriter.create(Row.getClassSchema(), out);

    for (int i = 0; i < rows; i++)
    {
      fileWriter.append(new Row(rnd.nextInt(), "row" + String.valueOf(rnd.nextInt())));
    }
    fileWriter.close();

    return avro;
  }

  public Path createTempCsv(int rows) throws IOException
  {
    final ThreadLocalRandom rnd = ThreadLocalRandom.current();

    final Path csv = Files.createTempFile(tmpIngestStage, "data", ".csv");
    final String fileName = csv.getFileName().toString();

    try (Writer w = Files.newBufferedWriter(csv, StandardCharsets.UTF_8))
    {
      for (int i = 0; i < rows; i++)
      {
        w.write(String.valueOf(i));
        w.write(",");
        w.write("row-");
        w.write(String.valueOf(i));
        w.write(",");
        w.write(String.valueOf(rnd.nextLong()));
        w.write(",");
        w.write(fileName);
        w.write("\n");
      }
    }
    return csv;
  }

  public int doQuery(String query)
      throws SQLException
  {
    return doQuery(query, connection);
  }

  public int doQuery(String query,
                     Connection connection)
      throws SQLException
  {
    say("query: " + query);
    try (Statement statement = connection.createStatement())
    {
      try (ResultSet resultSet = statement.executeQuery(query))
      {
        return dumpResultSet(resultSet);
      }
    }

  }

  private static int dumpResultSet(ResultSet resultSet) throws SQLException
  {
    final ResultSetMetaData md = resultSet.getMetaData();
    final int col_cnt = md.getColumnCount();
    int row_cnt = 0;
    while (resultSet.next())
    {
      final List row = new ArrayList<>(col_cnt);
      for (int i = 1; i <= col_cnt; i++)
      {
        final Object cell = resultSet.getObject(i);
        row.add(cell);
      }
      say("  result row = " + row);
      row_cnt++;
    }
    return row_cnt;
  }

  private static Connection getConnection()
      throws SQLException
  {
    return TestConnectionUtil.getConnection();
  }

  private static Connection getAdminConnection()
      throws SQLException
  {
    return TestConnectionUtil.getConnection(
        TestConnectionUtil.ADMIN_USER,
        TestConnectionUtil.ADMIN_ACCOUNT);
  }


  protected static void say(String arg)
  {
    System.out.println(System.currentTimeMillis() + ":" +
                           Thread.currentThread().getId() + "  " + arg);
  }

  //more or less copied from server side...
  public static class IngestPushFilesRequest {
    public List files;

    public static final class IngestFile
    {
      public String path;
      public Long size;  //optional
    }
  }


}