embulk-input-gcs 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ package org.embulk.input.gcs;
2
+
3
+ import com.google.cloud.ReadChannel;
4
+ import com.google.cloud.RestorableState;
5
+ import com.google.cloud.storage.Blob;
6
+ import com.google.cloud.storage.Storage;
7
+ import com.google.common.base.Charsets;
8
+ import com.google.common.io.Files;
9
+ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
10
+ import org.embulk.EmbulkTestRuntime;
11
+ import org.embulk.spi.util.ResumableInputStream;
12
+ import org.junit.Rule;
13
+ import org.junit.Test;
14
+ import org.mockito.Mockito;
15
+
16
+ import java.io.ByteArrayOutputStream;
17
+ import java.io.File;
18
+ import java.io.IOException;
19
+ import java.nio.ByteBuffer;
20
+ import java.nio.channels.FileChannel;
21
+ import java.nio.file.Paths;
22
+
23
+ import static org.junit.Assert.assertEquals;
24
+ import static org.junit.Assert.fail;
25
+ import static org.mockito.ArgumentMatchers.any;
26
+ import static org.mockito.ArgumentMatchers.eq;
27
+
28
+ public class TestInputStreamReopener
29
+ {
30
+ private static class MockReadChannel implements ReadChannel
31
+ {
32
+ private FileChannel ch;
33
+
34
+ MockReadChannel(FileChannel ch)
35
+ {
36
+ this.ch = ch;
37
+ }
38
+
39
+ @Override
40
+ public boolean isOpen()
41
+ {
42
+ return this.ch.isOpen();
43
+ }
44
+
45
+ @Override
46
+ public void close()
47
+ {
48
+ try {
49
+ this.ch.close();
50
+ }
51
+ catch (IOException ignored) {
52
+ }
53
+ }
54
+
55
+ @Override
56
+ public void seek(long position) throws IOException
57
+ {
58
+ this.ch.position(position);
59
+ }
60
+
61
+ @Override
62
+ public void setChunkSize(int chunkSize)
63
+ {
64
+ // no-op
65
+ }
66
+
67
+ @Override
68
+ public RestorableState<ReadChannel> capture()
69
+ {
70
+ return null;
71
+ }
72
+
73
+ @Override
74
+ public int read(ByteBuffer dst) throws IOException
75
+ {
76
+ return this.ch.read(dst);
77
+ }
78
+ }
79
+
80
+ private static final String SAMPLE_PATH = TestInputStreamReopener.class.getResource("/sample_01.csv").getPath();
81
+
82
+ @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
83
+ @Rule
84
+ public EmbulkTestRuntime runtime = new EmbulkTestRuntime();
85
+
86
+ @Test
87
+ public void testResume()
88
+ {
89
+ final String bucket = "any_bucket";
90
+ final String key = "any_file";
91
+
92
+ final Storage client = mockStorage();
93
+
94
+ final SingleFileProvider.InputStreamReopener reopener = new SingleFileProvider.InputStreamReopener(client, bucket, key);
95
+ final byte[] buf = new byte[200];
96
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
97
+ try (final ResumableInputStream ris = new ResumableInputStream(reopener)) {
98
+ int len;
99
+ while ((len = ris.read(buf)) != -1) {
100
+ out.write(buf, 0, len);
101
+ }
102
+ // read from resumable input stream
103
+ String content = out.toString("UTF-8");
104
+ // assert content
105
+ assertString(content);
106
+ }
107
+ catch (IOException e) {
108
+ e.printStackTrace();
109
+ fail("Should not throw");
110
+ }
111
+ }
112
+
113
+ private Storage mockStorage()
114
+ {
115
+ Blob blob = Mockito.mock(Blob.class);
116
+ // mock Storage to return ReadChannel
117
+ Storage client = Mockito.mock(Storage.class);
118
+ Mockito.doReturn(blob).when(client).get(eq("any_bucket"), eq("any_file"));
119
+ // to return new instance every time (can't re-use channel, because it'll be closed)
120
+ Mockito.doAnswer(invocation -> new MockReadChannel(mockChannel())).when(blob).reader();
121
+ return client;
122
+ }
123
+
124
+ /**
125
+ * Return a mock FileChannel, with simulated error during reads
126
+ *
127
+ * @return
128
+ * @throws IOException
129
+ */
130
+ private static FileChannel mockChannel() throws IOException
131
+ {
132
+ FileChannel ch = Mockito.spy(FileChannel.open(Paths.get(SAMPLE_PATH)));
133
+ // success -> error -> success -> error...
134
+ Mockito.doCallRealMethod()
135
+ .doThrow(new IOException("Fake IOException, going to resume"))
136
+ .when(ch).read(any(ByteBuffer.class));
137
+ return ch;
138
+ }
139
+
140
+ private static void assertString(final String actual) throws IOException
141
+ {
142
+ final String expected = Files.asCharSource(new File(SAMPLE_PATH), Charsets.UTF_8).read();
143
+ assertEquals(expected, actual);
144
+ }
145
+ }
@@ -0,0 +1,164 @@
1
+ package org.embulk.input.gcs;
2
+
3
+ import com.google.api.client.auth.oauth2.TokenResponseException;
4
+ import com.google.api.client.googleapis.json.GoogleJsonError;
5
+ import com.google.api.client.googleapis.json.GoogleJsonResponseException;
6
+ import com.google.api.client.http.HttpHeaders;
7
+ import com.google.api.client.http.HttpRequest;
8
+ import com.google.api.client.http.HttpTransport;
9
+ import com.google.api.client.http.LowLevelHttpRequest;
10
+ import com.google.api.client.http.LowLevelHttpResponse;
11
+ import com.google.api.client.json.Json;
12
+ import com.google.api.client.json.jackson2.JacksonFactory;
13
+ import com.google.api.client.testing.http.HttpTesting;
14
+ import com.google.api.client.testing.http.MockHttpTransport;
15
+ import com.google.api.client.testing.http.MockLowLevelHttpRequest;
16
+ import com.google.api.client.testing.http.MockLowLevelHttpResponse;
17
+ import com.google.cloud.storage.StorageException;
18
+ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
19
+ import org.embulk.EmbulkTestRuntime;
20
+ import org.embulk.spi.Exec;
21
+ import org.junit.Before;
22
+ import org.junit.Rule;
23
+ import org.junit.Test;
24
+ import org.mockito.Mockito;
25
+
26
+ import java.io.IOException;
27
+
28
+ import static org.embulk.input.gcs.RetryUtils.withRetry;
29
+ import static org.hamcrest.core.IsInstanceOf.instanceOf;
30
+ import static org.junit.Assert.assertEquals;
31
+ import static org.junit.Assert.assertFalse;
32
+ import static org.junit.Assert.assertNull;
33
+ import static org.junit.Assert.assertThat;
34
+ import static org.junit.Assert.assertTrue;
35
+
36
+ public class TestRetryUtils
37
+ {
38
+ @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
39
+ @Rule
40
+ public EmbulkTestRuntime runtime = new EmbulkTestRuntime();
41
+
42
+ private RetryUtils.DefaultRetryable<Object> mock;
43
+
44
+ @Before
45
+ public void setUp()
46
+ {
47
+ mock = new RetryUtils.DefaultRetryable<Object>()
48
+ {
49
+ @Override
50
+ public Object call()
51
+ {
52
+ return null;
53
+ }
54
+ };
55
+ }
56
+
57
+ @Test
58
+ public void testRetryable() throws IOException
59
+ {
60
+ // verify that #isRetryable() returns false for below cases:
61
+ // - GoogleJsonResponseException && details.code == 4xx
62
+ assertFalse(mock.isRetryableException(fakeJsonException(400, "fake_400_ex", null)));
63
+ // - TokenResponseException && statusCode == 4xx
64
+ assertFalse(mock.isRetryableException(fakeTokenException(400, "{}")));
65
+ assertFalse(mock.isRetryableException(fakeTokenException(401, "{\"foo\":\"bar\"}")));
66
+ assertFalse(mock.isRetryableException(fakeTokenException(403, "{ \"error_description\": \"Invalid...\"}")));
67
+ // return true
68
+ // - GoogleJsonResponseException && details.code = 5xx
69
+ assertTrue(mock.isRetryableException(fakeJsonException(500, "fake_500_ex", null)));
70
+ // - GoogleJsonResponseException && details == null && content != null
71
+ assertTrue(mock.isRetryableException(fakeJsonExceptionWithoutDetails(400, "fake_400_ex", "this content will make it retry-able")));
72
+ // - TokenResponseException && statusCode = 5xx
73
+ assertTrue(mock.isRetryableException(fakeTokenException(500, "{}")));
74
+ // - TokenResponseException && details.errorDescription contains 'Invalid JWT'
75
+ assertTrue(mock.isRetryableException(fakeTokenException(403, "{ \"error_description\": \"Invalid JWT...\"}")));
76
+ }
77
+
78
+ @Test
79
+ public void testWithRetry() throws Exception
80
+ {
81
+ mock = Mockito.spy(mock);
82
+ Exception ex = new StorageException(403, "Fake Exception");
83
+ Mockito.doThrow(ex).doThrow(ex).doReturn(null).when(mock).call();
84
+
85
+ Object result = withRetry(params(), mock);
86
+ assertNull(result);
87
+ Mockito.verify(mock, Mockito.times(3)).call();
88
+ }
89
+
90
+ @Test
91
+ public void testWithRetryGiveUp()
92
+ {
93
+ final String expectMsg = "Will retry and give up";
94
+ mock = new RetryUtils.DefaultRetryable<Object>()
95
+ {
96
+ @Override
97
+ public Object call()
98
+ {
99
+ throw new IllegalStateException(expectMsg);
100
+ }
101
+ };
102
+ try {
103
+ withRetry(params(), mock);
104
+ }
105
+ catch (RuntimeException e) {
106
+ // root cause -> RetryGiveUpException -> RuntimeException
107
+ Throwable rootCause = e.getCause().getCause();
108
+ assertEquals(expectMsg, rootCause.getMessage());
109
+ assertThat(rootCause, instanceOf(IllegalStateException.class));
110
+ }
111
+ }
112
+
113
+ private static RetryUtils.Task params()
114
+ {
115
+ return Exec.newConfigSource().set("initial_retry_interval_millis", 1).loadConfig(RetryUtils.Task.class);
116
+ }
117
+
118
+ private static GoogleJsonResponseException fakeJsonException(final int code, final String message, final String content)
119
+ {
120
+ GoogleJsonResponseException.Builder builder = new GoogleJsonResponseException.Builder(code, message, new HttpHeaders());
121
+ builder.setContent(content);
122
+ return new GoogleJsonResponseException(builder, fakeJsonError(code, message));
123
+ }
124
+
125
+ private static GoogleJsonResponseException fakeJsonExceptionWithoutDetails(final int code, final String message, final String content)
126
+ {
127
+ GoogleJsonResponseException.Builder builder = new GoogleJsonResponseException.Builder(code, message, new HttpHeaders());
128
+ builder.setContent(content);
129
+ return new GoogleJsonResponseException(builder, null);
130
+ }
131
+
132
+ private static GoogleJsonError fakeJsonError(final int code, final String message)
133
+ {
134
+ GoogleJsonError error = new GoogleJsonError();
135
+ error.setCode(code);
136
+ error.setMessage(message);
137
+ return error;
138
+ }
139
+
140
+ private static TokenResponseException fakeTokenException(final int code, final String content) throws IOException
141
+ {
142
+ HttpTransport transport = new MockHttpTransport() {
143
+ @Override
144
+ public LowLevelHttpRequest buildRequest(String method, String url)
145
+ {
146
+ return new MockLowLevelHttpRequest() {
147
+ @Override
148
+ public LowLevelHttpResponse execute()
149
+ {
150
+ MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
151
+ response.addHeader("custom_header", "value");
152
+ response.setStatusCode(code);
153
+ response.setContentType(Json.MEDIA_TYPE);
154
+ response.setContent(content);
155
+ return response;
156
+ }
157
+ };
158
+ }
159
+ };
160
+ HttpRequest request = transport.createRequestFactory().buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
161
+ request.setThrowExceptionOnExecuteError(false);
162
+ return TokenResponseException.from(JacksonFactory.getDefaultInstance(), request.execute());
163
+ }
164
+ }
Binary file
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: embulk-input-gcs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Satoshi Akama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-26 00:00:00.000000000 Z
11
+ date: 2019-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ~>
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '1.0'
19
19
  name: bundler
@@ -21,13 +21,13 @@ dependencies:
21
21
  type: :development
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - '>='
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '10.0'
33
33
  name: rake
@@ -35,21 +35,56 @@ dependencies:
35
35
  type: :development
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
- description: Reads files stored on Google Cloud Storage (Standard, Durable Reduced Availability or Nearline)
41
+ description: Reads files stored on Google Cloud Storage (Standard, Durable Reduced
42
+ Availability or Nearline)
42
43
  email:
43
44
  - satoshiakama@gmail.com
44
45
  executables: []
45
46
  extensions: []
46
47
  extra_rdoc_files: []
47
48
  files:
48
- - .gitignore
49
- - .travis.yml
49
+ - ".gitignore"
50
+ - ".travis.yml"
50
51
  - CHANGELOG.md
51
52
  - README.md
52
53
  - build.gradle
54
+ - classpath/animal-sniffer-annotations-1.14.jar
55
+ - classpath/api-common-1.7.0.jar
56
+ - classpath/checker-compat-qual-2.5.2.jar
57
+ - classpath/commons-codec-1.10.jar
58
+ - classpath/commons-logging-1.2.jar
59
+ - classpath/embulk-input-gcs-0.3.1-shadow.jar
60
+ - classpath/error_prone_annotations-2.1.3.jar
61
+ - classpath/gax-1.35.1.jar
62
+ - classpath/gax-httpjson-0.52.1.jar
63
+ - classpath/google-api-client-1.27.0.jar
64
+ - classpath/google-api-services-storage-v1-rev20181013-1.27.0.jar
65
+ - classpath/google-auth-library-credentials-0.12.0.jar
66
+ - classpath/google-auth-library-oauth2-http-0.12.0.jar
67
+ - classpath/google-cloud-core-1.56.0.jar
68
+ - classpath/google-cloud-core-http-1.56.0.jar
69
+ - classpath/google-http-client-1.27.0.jar
70
+ - classpath/google-http-client-appengine-1.27.0.jar
71
+ - classpath/google-http-client-jackson2-1.27.0.jar
72
+ - classpath/google-oauth-client-1.27.0.jar
73
+ - classpath/grpc-context-1.12.0.jar
74
+ - classpath/gson-2.7.jar
75
+ - classpath/httpclient-4.5.5.jar
76
+ - classpath/httpcore-4.4.9.jar
77
+ - classpath/j2objc-annotations-1.1.jar
78
+ - classpath/jackson-core-2.9.6.jar
79
+ - classpath/javax.annotation-api-1.2.jar
80
+ - classpath/jsr305-3.0.2.jar
81
+ - classpath/opencensus-api-0.15.0.jar
82
+ - classpath/opencensus-contrib-http-util-0.15.0.jar
83
+ - classpath/proto-google-common-protos-1.12.0.jar
84
+ - classpath/proto-google-iam-v1-0.12.0.jar
85
+ - classpath/protobuf-java-3.6.1.jar
86
+ - classpath/protobuf-java-util-3.6.1.jar
87
+ - classpath/threetenbp-1.3.3.jar
53
88
  - config/checkstyle/checkstyle.xml
54
89
  - config/checkstyle/default.xml
55
90
  - gradle/wrapper/gradle-wrapper.jar
@@ -57,31 +92,21 @@ files:
57
92
  - gradlew
58
93
  - gradlew.bat
59
94
  - lib/embulk/input/gcs.rb
60
- - secretkeys.tar
61
- - secretkeys.tar.enc
62
95
  - settings.gradle
96
+ - src/main/java/org/embulk/input/gcs/AuthUtils.java
63
97
  - src/main/java/org/embulk/input/gcs/FileList.java
64
- - src/main/java/org/embulk/input/gcs/GcsAuthentication.java
65
98
  - src/main/java/org/embulk/input/gcs/GcsFileInput.java
66
99
  - src/main/java/org/embulk/input/gcs/GcsFileInputPlugin.java
67
100
  - src/main/java/org/embulk/input/gcs/PluginTask.java
101
+ - src/main/java/org/embulk/input/gcs/RetryUtils.java
68
102
  - src/main/java/org/embulk/input/gcs/SingleFileProvider.java
69
- - src/test/java/org/embulk/input/gcs/TestGcsAuthentication.java
103
+ - src/test/java/org/embulk/input/gcs/TestAuthUtils.java
70
104
  - src/test/java/org/embulk/input/gcs/TestGcsFileInputPlugin.java
105
+ - src/test/java/org/embulk/input/gcs/TestInputStreamReopener.java
106
+ - src/test/java/org/embulk/input/gcs/TestRetryUtils.java
71
107
  - src/test/resources/sample_01.csv
72
108
  - src/test/resources/sample_02.csv
73
- - src/test/resources/secretkeys.tar.enc
74
- - classpath/google-api-client-1.21.0.jar
75
- - classpath/httpclient-4.0.1.jar
76
- - classpath/jsr305-1.3.9.jar
77
- - classpath/commons-logging-1.1.1.jar
78
- - classpath/google-api-services-storage-v1-rev59-1.21.0.jar
79
- - classpath/google-http-client-1.21.0.jar
80
- - classpath/google-oauth-client-1.21.0.jar
81
- - classpath/commons-codec-1.3.jar
82
- - classpath/google-http-client-jackson2-1.21.0.jar
83
- - classpath/httpcore-4.0.1.jar
84
- - classpath/embulk-input-gcs-0.3.0.jar
109
+ - src/test/resources/secrets.tar.enc
85
110
  homepage: https://github.com/embulk/embulk-input-gcs
86
111
  licenses:
87
112
  - Apache-2.0
@@ -92,17 +117,17 @@ require_paths:
92
117
  - lib
93
118
  required_ruby_version: !ruby/object:Gem::Requirement
94
119
  requirements:
95
- - - '>='
120
+ - - ">="
96
121
  - !ruby/object:Gem::Version
97
122
  version: '0'
98
123
  required_rubygems_version: !ruby/object:Gem::Requirement
99
124
  requirements:
100
- - - '>='
125
+ - - ">="
101
126
  - !ruby/object:Gem::Version
102
127
  version: '0'
103
128
  requirements: []
104
129
  rubyforge_project:
105
- rubygems_version: 2.1.9
130
+ rubygems_version: 2.6.13
106
131
  signing_key:
107
132
  specification_version: 4
108
133
  summary: Google Cloud Storage input plugin for Embulk