Skip to content

Commit f612567

Browse files
authored
Merge pull request #83 from trocco-io/add-rety-for-copy-task
Add retry logic for COPY operation in SnowflakeCopyBatchInsert with configurable max retries
2 parents b7ab45d + 7a3f515 commit f612567

File tree

3 files changed

+60
-24
lines changed

3 files changed

+60
-24
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Snowflake output plugin for Embulk loads records to Snowflake.
2323
- **retry_limit**: max retry count for database operations (integer, default: 12). When intermediate table to create already created by another process, this plugin will retry with another table name to avoid collision.
2424
- **retry_wait**: initial retry wait time in milliseconds (integer, default: 1000 (1 second))
2525
- **max_retry_wait**: upper limit of retry wait, which will be doubled at every retry (integer, default: 1800000 (30 minutes))
26+
- **max_upload_retries**: maximum number of retries for file upload to Snowflake (integer, default: 3).
27+
- **max_copy_retries**: maximum number of retries for COPY operations in Snowflake (integer, default: 3). Retries occur when transient errors such as communication failures happen during the COPY process.
2628
- **mode**: "insert", "insert_direct", "truncate_insert", "replace" or "merge". See below. (string, required)
2729
- **merge_keys**: key column names for merging records in merge mode (string array, required in merge mode if table doesn't have primary key)
2830
- **merge_rule**: list of column assignments for updating existing records used in merge mode, for example `"foo" = T."foo" + S."foo"` (`T` means target table and `S` means source table). (string array, default: always overwrites with new values)

src/main/java/org/embulk/output/SnowflakeOutputPlugin.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public interface SnowflakePluginTask extends PluginTask {
7575
@ConfigDefault("3")
7676
public int getMaxUploadRetries();
7777

78+
@Config("max_copy_retries")
79+
@ConfigDefault("3")
80+
public int getMaxCopyRetries();
81+
7882
@Config("empty_field_as_null")
7983
@ConfigDefault("true")
8084
public boolean getEmtpyFieldAsNull();
@@ -321,6 +325,7 @@ protected BatchInsert newBatchInsert(PluginTask task, Optional<MergeConfig> merg
321325
pluginTask.getCopyIntoCSVColumnNumbers(),
322326
false,
323327
pluginTask.getMaxUploadRetries(),
328+
pluginTask.getMaxCopyRetries(),
324329
pluginTask.getEmtpyFieldAsNull());
325330
}
326331

src/main/java/org/embulk/output/snowflake/SnowflakeCopyBatchInsert.java

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class SnowflakeCopyBatchInsert implements BatchInsert {
2727
protected static final String newLineString = "\n";
2828
protected static final String delimiterString = "\t";
2929
private final int maxUploadRetries;
30+
private final int maxCopyRetries;
3031

3132
private SnowflakeOutputConnection connection = null;
3233
private TableIdentifier tableIdentifier = null;
@@ -51,6 +52,7 @@ public SnowflakeCopyBatchInsert(
5152
int[] copyIntoCSVColumnNumbers,
5253
boolean deleteStageFile,
5354
int maxUploadRetries,
55+
int maxCopyRetries,
5456
boolean emptyFieldAsNull)
5557
throws IOException {
5658
this.index = 0;
@@ -63,6 +65,7 @@ public SnowflakeCopyBatchInsert(
6365
this.deleteStageFile = deleteStageFile;
6466
this.uploadAndCopyFutures = new ArrayList();
6567
this.maxUploadRetries = maxUploadRetries;
68+
this.maxCopyRetries = maxCopyRetries;
6669
this.emptyFieldAsNull = emptyFieldAsNull;
6770
}
6871

@@ -270,7 +273,8 @@ public void flush() throws IOException, SQLException {
270273
Future<Void> uploadFuture = executorService.submit(uploadTask);
271274
uploadAndCopyFutures.add(uploadFuture);
272275

273-
CopyTask copyTask = new CopyTask(uploadFuture, snowflakeStageFileName, emptyFieldAsNull);
276+
CopyTask copyTask =
277+
new CopyTask(uploadFuture, snowflakeStageFileName, emptyFieldAsNull, maxCopyRetries);
274278
uploadAndCopyFutures.add(executorService.submit(copyTask));
275279

276280
fileCount++;
@@ -404,40 +408,60 @@ private class CopyTask implements Callable<Void> {
404408
private final Future<Void> uploadFuture;
405409
private final String snowflakeStageFileName;
406410
private final boolean emptyFieldAsNull;
411+
private final int maxCopyRetries;
407412

408413
public CopyTask(
409-
Future<Void> uploadFuture, String snowflakeStageFileName, boolean emptyFieldAsNull) {
414+
Future<Void> uploadFuture,
415+
String snowflakeStageFileName,
416+
boolean emptyFieldAsNull,
417+
int maxCopyRetries) {
410418
this.uploadFuture = uploadFuture;
411419
this.snowflakeStageFileName = snowflakeStageFileName;
412420
this.emptyFieldAsNull = emptyFieldAsNull;
421+
this.maxCopyRetries = maxCopyRetries;
413422
}
414423

415424
public Void call() throws SQLException, InterruptedException, ExecutionException {
416425
try {
417426
uploadFuture.get();
418427

419-
SnowflakeOutputConnection con = (SnowflakeOutputConnection) connector.connect(true);
420-
try {
421-
logger.info("Running COPY from file {}", snowflakeStageFileName);
422-
423-
long startTime = System.currentTimeMillis();
424-
con.runCopy(
425-
tableIdentifier,
426-
stageIdentifier,
427-
snowflakeStageFileName,
428-
copyIntoTableColumnNames,
429-
copyIntoCSVColumnNumbers,
430-
delimiterString,
431-
emptyFieldAsNull);
432-
433-
double seconds = (System.currentTimeMillis() - startTime) / 1000.0;
434-
435-
logger.info(
436-
String.format(
437-
"Loaded file %s (%.2f seconds for COPY)", snowflakeStageFileName, seconds));
438-
439-
} finally {
440-
con.close();
428+
int retries = 0;
429+
while (true) {
430+
try (SnowflakeOutputConnection con =
431+
(SnowflakeOutputConnection) connector.connect(true)) {
432+
logger.info("Running COPY from file {}", snowflakeStageFileName);
433+
434+
long startTime = System.currentTimeMillis();
435+
con.runCopy(
436+
tableIdentifier,
437+
stageIdentifier,
438+
snowflakeStageFileName,
439+
copyIntoTableColumnNames,
440+
copyIntoCSVColumnNumbers,
441+
delimiterString,
442+
emptyFieldAsNull);
443+
444+
double seconds = (System.currentTimeMillis() - startTime) / 1000.0;
445+
446+
logger.info(
447+
String.format(
448+
"Loaded file %s (%.2f seconds for COPY)", snowflakeStageFileName, seconds));
449+
450+
break;
451+
} catch (SQLException e) {
452+
if (!isRetryableForCopyTask(e)) {
453+
throw e;
454+
}
455+
456+
retries++;
457+
if (retries > this.maxCopyRetries) {
458+
throw e;
459+
}
460+
logger.warn(
461+
String.format(
462+
"Copy error %s file %s retries: %d", e, snowflakeStageFileName, retries));
463+
Thread.sleep(retries * retries * 1000);
464+
}
441465
}
442466
} finally {
443467
if (deleteStageFile) {
@@ -447,5 +471,10 @@ public Void call() throws SQLException, InterruptedException, ExecutionException
447471

448472
return null;
449473
}
474+
475+
private boolean isRetryableForCopyTask(SQLException e) {
476+
String message = e.getMessage();
477+
return message != null && message.contains("JDBC driver encountered communication error");
478+
}
450479
}
451480
}

0 commit comments

Comments
 (0)