Skip to content

Commit eed62e6

Browse files
authored
Merge branch 'main' into replace-bytebuffer-array-call
2 parents 752536f + 4cb4250 commit eed62e6

13 files changed

Lines changed: 365 additions & 174 deletions

File tree

pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,15 @@
110110
<version>${zstdVersion}</version>
111111
</dependency>
112112
<dependency>
113-
<groupId>com.squareup.okhttp</groupId>
113+
<groupId>com.squareup.okhttp3</groupId>
114114
<artifactId>okhttp</artifactId>
115-
<version>2.7.5</version>
115+
<version>4.12.0</version>
116+
</dependency>
117+
<dependency>
118+
<groupId>com.squareup.okhttp3</groupId>
119+
<artifactId>mockwebserver</artifactId>
120+
<version>4.12.0</version>
121+
<scope>test</scope>
116122
</dependency>
117123
<!-- JUnit 4 dependency for backward compatibility if needed -->
118124
<dependency>

src/main/java/dev/zarr/zarrjava/core/ArrayMetadata.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ public static Object parseFillValue(Object fillValue, @Nonnull DataType dataType
3636
}
3737
boolean dataTypeIsBool = dataType == dev.zarr.zarrjava.v3.DataType.BOOL || dataType == dev.zarr.zarrjava.v2.DataType.BOOL;
3838
boolean dataTypeIsByte = dataType == dev.zarr.zarrjava.v3.DataType.INT8 || dataType == dev.zarr.zarrjava.v2.DataType.INT8 || dataType == dev.zarr.zarrjava.v3.DataType.UINT8 || dataType == dev.zarr.zarrjava.v2.DataType.UINT8;
39-
boolean dataTypeIsShort = dataType == dev.zarr.zarrjava.v3.DataType.INT16 || dataType == dev.zarr.zarrjava.v2.DataType.INT16 || dataType == dev.zarr.zarrjava.v3.DataType.UINT16 || dataType == dev.zarr.zarrjava.v2.DataType.UINT16;
40-
boolean dataTypeIsInt = dataType == dev.zarr.zarrjava.v3.DataType.INT32 || dataType == dev.zarr.zarrjava.v2.DataType.INT32 || dataType == dev.zarr.zarrjava.v3.DataType.UINT32 || dataType == dev.zarr.zarrjava.v2.DataType.UINT32;
41-
boolean dataTypeIsLong = dataType == dev.zarr.zarrjava.v3.DataType.INT64 || dataType == dev.zarr.zarrjava.v2.DataType.INT64 || dataType == dev.zarr.zarrjava.v3.DataType.UINT64 || dataType == dev.zarr.zarrjava.v2.DataType.UINT64;
42-
boolean dataTypeIsFloat = dataType == dev.zarr.zarrjava.v3.DataType.FLOAT32 || dataType == dev.zarr.zarrjava.v2.DataType.FLOAT32;
43-
boolean dataTypeIsDouble = dataType == dev.zarr.zarrjava.v3.DataType.FLOAT64 || dataType == dev.zarr.zarrjava.v2.DataType.FLOAT64;
39+
boolean dataTypeIsShort = dataType == dev.zarr.zarrjava.v3.DataType.INT16 || dataType == dev.zarr.zarrjava.v2.DataType.INT16 || dataType == dev.zarr.zarrjava.v2.DataType.INT16_BE || dataType == dev.zarr.zarrjava.v3.DataType.UINT16 || dataType == dev.zarr.zarrjava.v2.DataType.UINT16|| dataType == dev.zarr.zarrjava.v2.DataType.UINT16_BE;
40+
boolean dataTypeIsInt = dataType == dev.zarr.zarrjava.v3.DataType.INT32 || dataType == dev.zarr.zarrjava.v2.DataType.INT32 || dataType == dev.zarr.zarrjava.v2.DataType.INT32_BE || dataType == dev.zarr.zarrjava.v3.DataType.UINT32 || dataType == dev.zarr.zarrjava.v2.DataType.UINT32 || dataType == dev.zarr.zarrjava.v2.DataType.UINT32_BE;
41+
boolean dataTypeIsLong = dataType == dev.zarr.zarrjava.v3.DataType.INT64 || dataType == dev.zarr.zarrjava.v2.DataType.INT64 || dataType == dev.zarr.zarrjava.v2.DataType.INT64_BE || dataType == dev.zarr.zarrjava.v3.DataType.UINT64 || dataType == dev.zarr.zarrjava.v2.DataType.UINT64 || dataType == dev.zarr.zarrjava.v2.DataType.UINT64_BE;
42+
boolean dataTypeIsFloat = dataType == dev.zarr.zarrjava.v3.DataType.FLOAT32 || dataType == dev.zarr.zarrjava.v2.DataType.FLOAT32 || dataType == dev.zarr.zarrjava.v2.DataType.FLOAT32_BE;
43+
boolean dataTypeIsDouble = dataType == dev.zarr.zarrjava.v3.DataType.FLOAT64 || dataType == dev.zarr.zarrjava.v2.DataType.FLOAT64 || dataType == dev.zarr.zarrjava.v2.DataType.FLOAT64_BE;
4444

4545
if (fillValue instanceof Boolean) {
4646
Boolean fillValueBool = (Boolean) fillValue;

src/main/java/dev/zarr/zarrjava/store/HttpStore.java

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package dev.zarr.zarrjava.store;
22

3-
import com.squareup.okhttp.*;
3+
import okhttp3.*;
44

55
import javax.annotation.Nonnull;
66
import javax.annotation.Nullable;
77
import java.io.FilterInputStream;
88
import java.io.IOException;
99
import java.io.InputStream;
1010
import java.nio.ByteBuffer;
11+
import java.time.Duration;
1112

1213
public class HttpStore implements Store {
1314

@@ -17,8 +18,16 @@ public class HttpStore implements Store {
1718
private final String uri;
1819

1920
public HttpStore(@Nonnull String uri) {
20-
this.httpClient = new OkHttpClient();
21+
this(uri, 60, 3, 1000);
22+
}
23+
24+
public HttpStore(@Nonnull String uri, int timeoutSeconds, int maxRetries, long retryDelayMs) {
2125
this.uri = uri;
26+
this.httpClient = new OkHttpClient.Builder()
27+
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
28+
.readTimeout(Duration.ofSeconds(timeoutSeconds))
29+
.addInterceptor(new RetryInterceptor(maxRetries, retryDelayMs))
30+
.build();
2231
}
2332

2433
String resolveKeys(String[] keys) {
@@ -37,9 +46,7 @@ String resolveKeys(String[] keys) {
3746

3847
@Nullable
3948
ByteBuffer get(Request request, String[] keys) {
40-
Call call = httpClient.newCall(request);
41-
try {
42-
Response response = call.execute();
49+
try (Response response = httpClient.newCall(request).execute()) {
4350
if (!response.isSuccessful()) {
4451
if (response.code() == 404) {
4552
return null;
@@ -49,12 +56,8 @@ ByteBuffer get(Request request, String[] keys) {
4956
keys,
5057
new IOException("HTTP request failed with status code: " + response.code() + " " + response.message()));
5158
}
52-
try (ResponseBody body = response.body()) {
53-
if (body == null) {
54-
return null;
55-
}
56-
return ByteBuffer.wrap(body.bytes());
57-
}
59+
ResponseBody body = response.body();
60+
return (body == null) ? null : ByteBuffer.wrap(body.bytes());
5861
} catch (IOException e) {
5962
throw StoreException.readFailed(this.toString(), keys, e);
6063
}
@@ -63,9 +66,7 @@ ByteBuffer get(Request request, String[] keys) {
6366
@Override
6467
public boolean exists(String[] keys) {
6568
Request request = new Request.Builder().head().url(resolveKeys(keys)).build();
66-
Call call = httpClient.newCall(request);
67-
try {
68-
Response response = call.execute();
69+
try (Response response = httpClient.newCall(request).execute()) {
6970
return response.isSuccessful();
7071
} catch (IOException e) {
7172
return false;
@@ -129,28 +130,33 @@ public InputStream getInputStream(String[] keys, long start, long end) {
129130
}
130131
Request request = new Request.Builder().url(resolveKeys(keys)).header(
131132
"Range", String.format("bytes=%d-%d", start, end - 1)).build();
132-
Call call = httpClient.newCall(request);
133+
133134
try {
134-
Response response = call.execute();
135+
// We do NOT use try-with-resources here because the stream must remain open
136+
Response response = httpClient.newCall(request).execute();
135137
if (!response.isSuccessful()) {
136138
if (response.code() == 404) {
139+
response.close();
137140
return null;
138141
}
139-
throw StoreException.readFailed(
140-
this.toString(),
141-
keys,
142-
new IOException("HTTP request failed with status code: " + response.code() + " " + response.message()));
142+
int code = response.code();
143+
String msg = response.message();
144+
response.close();
145+
throw StoreException.readFailed(this.toString(), keys,
146+
new IOException("HTTP request failed with status code: " + code + " " + msg));
143147
}
148+
144149
ResponseBody body = response.body();
145-
if (body == null) return null;
146-
InputStream stream = body.byteStream();
150+
if (body == null) {
151+
response.close();
152+
return null;
153+
}
147154

148-
// Ensure closing the stream also closes the response
149-
return new FilterInputStream(stream) {
155+
return new FilterInputStream(body.byteStream()) {
150156
@Override
151157
public void close() throws IOException {
152158
super.close();
153-
body.close();
159+
response.close(); // Closes both body and underlying connection
154160
}
155161
};
156162
} catch (IOException e) {
@@ -169,9 +175,7 @@ public long getSize(String[] keys) {
169175
.header("Accept-Encoding", "identity")
170176
.build();
171177

172-
Call call = httpClient.newCall(request);
173-
try {
174-
Response response = call.execute();
178+
try (Response response = httpClient.newCall(request).execute()) {
175179
if (!response.isSuccessful()) {
176180
return -1;
177181
}
@@ -193,4 +197,44 @@ public long getSize(String[] keys) {
193197
new IOException("Failed to get content length from HTTP HEAD request to: " + url, e));
194198
}
195199
}
196-
}
200+
201+
/**
202+
* Internal interceptor to handle retries for all HttpStore requests.
203+
*/
204+
private static class RetryInterceptor implements Interceptor {
205+
private final int maxRetries;
206+
private final long delay;
207+
208+
RetryInterceptor(int maxRetries, long delay) {
209+
this.maxRetries = maxRetries;
210+
this.delay = delay;
211+
}
212+
213+
@Override
214+
@Nonnull
215+
public Response intercept(@Nonnull Chain chain) throws IOException {
216+
Request request = chain.request();
217+
IOException lastException = null;
218+
219+
for (int i = 0; i <= maxRetries; i++) {
220+
try {
221+
if (i > 0) Thread.sleep(delay);
222+
Response response = chain.proceed(request);
223+
224+
// Retry on common transient server errors (502, 503, 504)
225+
if (response.isSuccessful() || response.code() == 404 || i == maxRetries || response.code() < 500) {
226+
return response;
227+
}
228+
response.close();
229+
} catch (IOException e) {
230+
lastException = e;
231+
if (i == maxRetries) throw e;
232+
} catch (InterruptedException e) {
233+
Thread.currentThread().interrupt();
234+
throw new IOException("Retry interrupted", e);
235+
}
236+
}
237+
throw lastException != null ? lastException : new IOException("Request failed after retries");
238+
}
239+
}
240+
}

src/main/java/dev/zarr/zarrjava/v2/ArrayMetadataBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,12 @@ public ArrayMetadata build() throws ZarrException {
150150
if (dataType == null) {
151151
throw new IllegalStateException("Please call `withDataType` first.");
152152
}
153-
153+
154154
// If chunks are not specified, calculate default chunks
155155
if (chunks == null) {
156156
chunks = Utils.calculateDefaultChunks(shape);
157157
}
158-
158+
159159
return new ArrayMetadata(
160160
2,
161161
shape,

src/main/java/dev/zarr/zarrjava/v2/DataType.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ public enum DataType implements dev.zarr.zarrjava.core.DataType {
1313
UINT32("u4", Endianness.LITTLE),
1414
UINT64("u8", Endianness.LITTLE),
1515
FLOAT32("f4", Endianness.LITTLE),
16-
FLOAT64("f8", Endianness.LITTLE);
16+
FLOAT64("f8", Endianness.LITTLE),
17+
INT16_BE("i2", Endianness.BIG),
18+
INT32_BE("i4", Endianness.BIG),
19+
INT64_BE("i8", Endianness.BIG),
20+
UINT16_BE("u2", Endianness.BIG),
21+
UINT32_BE("u4", Endianness.BIG),
22+
UINT64_BE("u8", Endianness.BIG),
23+
FLOAT32_BE("f4", Endianness.BIG),
24+
FLOAT64_BE("f8", Endianness.BIG);
1725

1826
private final String dtype;
1927
private final Endianness endianness;
@@ -41,22 +49,30 @@ public ucar.ma2.DataType getMA2DataType() {
4149
case INT8:
4250
return ucar.ma2.DataType.BYTE;
4351
case INT16:
52+
case INT16_BE:
4453
return ucar.ma2.DataType.SHORT;
4554
case INT32:
55+
case INT32_BE:
4656
return ucar.ma2.DataType.INT;
4757
case INT64:
58+
case INT64_BE:
4859
return ucar.ma2.DataType.LONG;
4960
case UINT8:
5061
return ucar.ma2.DataType.UBYTE;
5162
case UINT16:
63+
case UINT16_BE:
5264
return ucar.ma2.DataType.USHORT;
5365
case UINT32:
66+
case UINT32_BE:
5467
return ucar.ma2.DataType.UINT;
5568
case UINT64:
69+
case UINT64_BE:
5670
return ucar.ma2.DataType.ULONG;
5771
case FLOAT32:
72+
case FLOAT32_BE:
5873
return ucar.ma2.DataType.FLOAT;
5974
case FLOAT64:
75+
case FLOAT64_BE:
6076
return ucar.ma2.DataType.DOUBLE;
6177
default:
6278
throw new RuntimeException("Unreachable");

src/main/java/dev/zarr/zarrjava/v3/codec/core/ShardingIndexedCodec.java

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,7 @@ public ByteBuffer encode(final Array shardArray) throws ZarrException {
113113
extendArrayBy1(chunksPerShard, 2));
114114
final List<ByteBuffer> chunkBytesList = new ArrayList<>(chunkCount);
115115

116-
Arrays.stream(
117-
IndexingUtils.computeChunkCoords(shardMetadata.shape, shardMetadata.chunkShape))
116+
Arrays.stream(IndexingUtils.computeChunkCoords(shardMetadata.shape, shardMetadata.chunkShape))
118117
.parallel()
119118
.forEach(
120119
chunkCoords -> {
@@ -128,8 +127,10 @@ public ByteBuffer encode(final Array shardArray) throws ZarrException {
128127
null
129128
);
130129
if (MultiArrayUtils.allValuesEqual(chunkArray, shardMetadata.parsedFillValue)) {
131-
setValueFromShardIndexArray(shardIndexArray, chunkCoords, 0, -1);
132-
setValueFromShardIndexArray(shardIndexArray, chunkCoords, 1, -1);
130+
synchronized (chunkBytesList) {
131+
setValueFromShardIndexArray(shardIndexArray, chunkCoords, 0, -1);
132+
setValueFromShardIndexArray(shardIndexArray, chunkCoords, 1, -1);
133+
}
133134
} else {
134135
final ByteBuffer chunkBytes = codecPipeline.encode(chunkArray);
135136
synchronized (chunkBytesList) {
@@ -207,33 +208,32 @@ private Array decodeInternal(
207208
Utils.toLongArray(shape));
208209

209210
Arrays.stream(allChunkCoords)
210-
// .parallel()
211+
.parallel()
211212
.forEach(
212213
chunkCoords -> {
213214
try {
214215
final long chunkByteOffset = getValueFromShardIndexArray(shardIndexArray,
215216
chunkCoords, 0);
216217
final long chunkByteLength = getValueFromShardIndexArray(shardIndexArray,
217218
chunkCoords, 1);
218-
Array chunkArray = null;
219+
if (chunkByteOffset == -1 || chunkByteLength == -1) {
220+
return;
221+
}
219222
final IndexingUtils.ChunkProjection chunkProjection =
220223
IndexingUtils.computeProjection(chunkCoords, shardMetadata.shape,
221224
shardMetadata.chunkShape, offset, Utils.toLongArray(shape)
222225
);
223-
if (chunkByteOffset != -1 && chunkByteLength != -1) {
224-
final ByteBuffer chunkBytes = dataProvider.read(chunkByteOffset, chunkByteLength);
225-
if (chunkBytes == null) {
226-
throw new ZarrException(String.format("Could not load byte data for chunk %s",
227-
Arrays.toString(chunkCoords)));
228-
}
229-
chunkArray = codecPipeline.decode(chunkBytes);
226+
final ByteBuffer chunkBytes = dataProvider.read(chunkByteOffset, chunkByteLength);
227+
if (chunkBytes == null) {
228+
throw new ZarrException(String.format("Could not load byte data for chunk %s",
229+
Arrays.toString(chunkCoords)));
230230
}
231-
if (chunkArray == null) {
232-
chunkArray = shardMetadata.allocateFillValueChunk();
231+
Array chunkArray = codecPipeline.decode(chunkBytes);
232+
synchronized (outputArray) {
233+
MultiArrayUtils.copyRegion(chunkArray, chunkProjection.chunkOffset, outputArray,
234+
chunkProjection.outOffset, chunkProjection.shape
235+
);
233236
}
234-
MultiArrayUtils.copyRegion(chunkArray, chunkProjection.chunkOffset, outputArray,
235-
chunkProjection.outOffset, chunkProjection.shape
236-
);
237237
} catch (ZarrException e) {
238238
throw new RuntimeException(e);
239239
}

0 commit comments

Comments
 (0)