Skip to content
3 changes: 2 additions & 1 deletion src/main/java/dev/zarr/zarrjava/v2/Array.java
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ public Array setAttributes(Attributes newAttributes) throws ZarrException, IOExc
* @throws IOException throws IOException if the new metadata cannot be serialized
*/
public Array updateAttributes(Function<Attributes, Attributes> attributeMapper) throws ZarrException, IOException {
return setAttributes(attributeMapper.apply(metadata.attributes));
Attributes currentAttributes = metadata.attributes != null ? new Attributes(metadata.attributes) : new Attributes();
return setAttributes(attributeMapper.apply(currentAttributes));
}

@Override
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/zarr/zarrjava/v2/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ public Group setAttributes(Attributes newAttributes) throws ZarrException, IOExc
*/
public Group updateAttributes(Function<Attributes, Attributes> attributeMapper)
throws ZarrException, IOException {
return setAttributes(attributeMapper.apply(metadata.attributes));
Attributes currentAttributes = metadata.attributes != null ? new Attributes(metadata.attributes) : new Attributes();
return setAttributes(attributeMapper.apply(currentAttributes));
}


Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/zarr/zarrjava/v3/Array.java
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ public Array setAttributes(Attributes newAttributes) throws ZarrException, IOExc
* @throws IOException throws IOException if the new metadata cannot be serialized
*/
public Array updateAttributes(Function<Attributes, Attributes> attributeMapper) throws ZarrException, IOException {
return setAttributes(attributeMapper.apply(metadata.attributes));
Attributes currentAttributes = metadata.attributes != null ? new Attributes(metadata.attributes) : new Attributes();
return setAttributes(attributeMapper.apply(currentAttributes));
}

@Override
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/zarr/zarrjava/v3/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ private Group writeMetadata(GroupMetadata newGroupMetadata) throws IOException {
*/
public Group updateAttributes(Function<Attributes, Attributes> attributeMapper)
throws ZarrException, IOException {
return setAttributes(attributeMapper.apply(metadata.attributes));
Attributes currentAttributes = metadata.attributes != null ? new Attributes(metadata.attributes) : new Attributes();
return setAttributes(attributeMapper.apply(currentAttributes));
}

/**
Expand Down
154 changes: 154 additions & 0 deletions src/test/java/dev/zarr/zarrjava/ParallelWriteTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package dev.zarr.zarrjava;

import dev.zarr.zarrjava.store.FilesystemStore;
import dev.zarr.zarrjava.store.StoreHandle;
import dev.zarr.zarrjava.v3.Array;
import dev.zarr.zarrjava.v3.DataType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class ParallelWriteTest extends ZarrTest {

@Test
public void testParallelWriteDataSafety() throws IOException, ZarrException {
// Test internal parallelism of write method (using parallel=true)
Path path = TESTOUTPUT.resolve("parallel_write_safety");
StoreHandle storeHandle = new FilesystemStore(path).resolve();

int shape = 1000;
int chunk = 100;

Array array = Array.create(storeHandle, Array.metadataBuilder()
.withShape(shape, shape)
.withDataType(DataType.INT32)
.withChunkShape(chunk, chunk)
.withFillValue(0)
.build());

int[] data = new int[shape * shape];
// Fill with some deterministic pattern
for (int i = 0; i < shape * shape; i++) {
data[i] = i;
}

ucar.ma2.Array outputData = ucar.ma2.Array.factory(ucar.ma2.DataType.INT, new int[]{shape, shape}, data);

// Write in parallel
array.write(outputData, true);

// Read back
ucar.ma2.Array readData = array.read();
int[] readArr = (int[]) readData.get1DJavaArray(ucar.ma2.DataType.INT);

Assertions.assertArrayEquals(data, readArr, "Data read back should match data written in parallel");
}

@Test
public void testParallelWriteWithSharding() throws IOException, ZarrException {
// Test internal parallelism with Sharding (nested chunks + shared codec state potential)
Path path = TESTOUTPUT.resolve("parallel_write_sharding");
StoreHandle storeHandle = new FilesystemStore(path).resolve();

int shape = 128; // 128x128
int shardSize = 64; // Shards are 64x64
int innerChunk = 32; // Inner chunks 32x32

// Metadata with sharding
// With shape 128 and shardSize 64, we have 2x2 = 4 shards.
// Array.write(parallel=true) will likely process these shards concurrently.
dev.zarr.zarrjava.v3.ArrayMetadata metadata = Array.metadataBuilder()
.withShape(shape, shape)
.withDataType(DataType.INT32)
.withChunkShape(shardSize, shardSize) // This sets the shard shape (outer chunks)
.withCodecs(c -> c.withSharding(new int[]{innerChunk, innerChunk}, c2 -> c2.withBytes("LITTLE")))
.withFillValue(0)
.build();

Array array = Array.create(storeHandle, metadata);

int[] data = new int[shape * shape];
for (int i = 0; i < shape * shape; i++) {
data[i] = i;
}

ucar.ma2.Array outputData = ucar.ma2.Array.factory(ucar.ma2.DataType.INT, new int[]{shape, shape}, data);

// Write in parallel
array.write(outputData, true);

ucar.ma2.Array readData = array.read();
int[] readArr = (int[]) readData.get1DJavaArray(ucar.ma2.DataType.INT);

Assertions.assertArrayEquals(data, readArr, "Sharded data written in parallel should match");
}

@Test
public void testConcurrentWritesDifferentChunks() throws IOException, ZarrException, InterruptedException, ExecutionException {
// Test external parallelism (multiple threads calling write on same Array instance)
Path path = TESTOUTPUT.resolve("concurrent_write_safety");
StoreHandle storeHandle = new FilesystemStore(path).resolve();

int chunksX = 10;
int chunksY = 10;
int chunkSize = 50;
int shapeX = chunksX * chunkSize;
int shapeY = chunksY * chunkSize;

Array array = Array.create(storeHandle, Array.metadataBuilder()
.withShape(shapeX, shapeY)
.withDataType(DataType.INT32)
.withChunkShape(chunkSize, chunkSize)
.withFillValue(-1)
.build());

ExecutorService executor = Executors.newFixedThreadPool(8);
List<Callable<Void>> tasks = new ArrayList<>();

for (int i = 0; i < chunksX; i++) {
for (int j = 0; j < chunksY; j++) {
final int cx = i;
final int cy = j;
tasks.add(() -> {
int[] chunkData = new int[chunkSize * chunkSize];
int val = cx * chunksY + cy; // Unique value per chunk
java.util.Arrays.fill(chunkData, val);

ucar.ma2.Array ucarArray = ucar.ma2.Array.factory(ucar.ma2.DataType.INT, new int[]{chunkSize, chunkSize}, chunkData);

// Write to specific chunk offset
long[] offset = new long[]{cx * chunkSize, cy * chunkSize};
// Use internal parallelism false to isolate external concurrency test mechanism
array.write(offset, ucarArray, false);
return null;
});
}
}

List<Future<Void>> futures = executor.invokeAll(tasks);

for (Future<Void> f : futures) {
f.get(); // Check for exceptions
}
executor.shutdown();

// Verification
ucar.ma2.Array readData = array.read();
for (int i = 0; i < chunksX; i++) {
for (int j = 0; j < chunksY; j++) {
int expectedVal = i * chunksY + j;
int originX = i * chunkSize;
int originY = j * chunkSize;

// Verify a pixel in the chunk
int val = readData.getInt(readData.getIndex().set(originX, originY));
Assertions.assertEquals(expectedVal, val, "Value at chunk " + i + "," + j + " mismatch");
}
}
}
}
56 changes: 56 additions & 0 deletions src/test/java/dev/zarr/zarrjava/ZarrV2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,31 @@ public void testSetAndUpdateAttributes() throws IOException, ZarrException {
assertContainsTestAttributes(array.metadata().attributes());
}

@Test
public void testUpdateAttributesBehavior() throws IOException, ZarrException {
Comment thread
brokkoli71 marked this conversation as resolved.
StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve("testUpdateAttributesBehaviorV2");
ArrayMetadata arrayMetadata = Array.metadataBuilder()
.withShape(10, 10)
.withDataType(DataType.UINT8)
.withChunks(5, 5)
.withAttributes(new Attributes(b -> b.set("key1", "val1")))
.build();

Array array1 = Array.create(storeHandle, arrayMetadata);
Array array2 = array1.updateAttributes(attrs -> attrs.set("key2", "val2"));

Assertions.assertNotSame(array1, array2);
Assertions.assertEquals("val1", array1.metadata().attributes().get("key1"));
Assertions.assertNull(array1.metadata().attributes().get("key2"));

Assertions.assertEquals("val1", array2.metadata().attributes().get("key1"));
Assertions.assertEquals("val2", array2.metadata().attributes().get("key2"));

// Re-opening should show the updated attributes
Array array3 = Array.open(storeHandle);
Assertions.assertEquals("val2", array3.metadata().attributes().get("key2"));
}

@Test
public void testResizeArray() throws IOException, ZarrException {
int[] testData = new int[10 * 10];
Expand All @@ -357,6 +382,37 @@ public void testResizeArray() throws IOException, ZarrException {
int[] expectedData = new int[5 * 5];
Arrays.fill(expectedData, 1);
Assertions.assertArrayEquals(expectedData, (int[]) data.get1DJavaArray(ma2DataType));

Array reopenedArray = Array.open(storeHandle);
Assertions.assertArrayEquals(new int[]{20, 15}, reopenedArray.read().getShape());
}

@Test
public void testResizeArrayShrink() throws IOException, ZarrException {
int[] testData = new int[10 * 10];
Arrays.setAll(testData, p -> p);

StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve("testResizeArrayShrinkV2");
ArrayMetadata arrayMetadata = Array.metadataBuilder()
.withShape(10, 10)
.withDataType(DataType.UINT32)
.withChunks(5, 5)
.build();
ucar.ma2.DataType ma2DataType = arrayMetadata.dataType.getMA2DataType();
Array array = Array.create(storeHandle, arrayMetadata);
array.write(new long[]{0, 0}, ucar.ma2.Array.factory(ma2DataType, new int[]{10, 10}, testData));

array = array.resize(new long[]{5, 5});
Assertions.assertArrayEquals(new int[]{5, 5}, array.read().getShape());

ucar.ma2.Array data = array.read();
int[] expectedData = new int[5 * 5];
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
expectedData[i * 5 + j] = testData[i * 10 + j];
}
}
Assertions.assertArrayEquals(expectedData, (int[]) data.get1DJavaArray(ma2DataType));
}

@Test
Expand Down
56 changes: 56 additions & 0 deletions src/test/java/dev/zarr/zarrjava/ZarrV3Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,31 @@ public void testSetAndUpdateAttributes() throws IOException, ZarrException {
assertContainsTestAttributes(array.metadata().attributes());
}

@Test
public void testUpdateAttributesBehavior() throws IOException, ZarrException {
StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve("testUpdateAttributesBehaviorV3");
ArrayMetadata arrayMetadata = Array.metadataBuilder()
.withShape(10, 10)
.withDataType(DataType.UINT8)
.withChunkShape(5, 5)
.withAttributes(new Attributes(b -> b.set("key1", "val1")))
.build();

Array array1 = Array.create(storeHandle, arrayMetadata);
Array array2 = array1.updateAttributes(attrs -> attrs.set("key2", "val2"));

Assertions.assertNotSame(array1, array2);
Assertions.assertEquals("val1", array1.metadata().attributes().get("key1"));
Assertions.assertNull(array1.metadata().attributes().get("key2"));

Assertions.assertEquals("val1", array2.metadata().attributes().get("key1"));
Assertions.assertEquals("val2", array2.metadata().attributes().get("key2"));

// Re-opening should show the updated attributes
Array array3 = Array.open(storeHandle);
Assertions.assertEquals("val2", array3.metadata().attributes().get("key2"));
}

@Test
public void testResizeArray() throws IOException, ZarrException {
int[] testData = new int[10 * 10];
Expand All @@ -732,6 +757,37 @@ public void testResizeArray() throws IOException, ZarrException {
int[] expectedData = new int[5 * 5];
Arrays.fill(expectedData, 1);
Assertions.assertArrayEquals(expectedData, (int[]) data.get1DJavaArray(ma2DataType));

Array reopenedArray = Array.open(storeHandle);
Assertions.assertArrayEquals(new int[]{20, 15}, reopenedArray.read().getShape());
}

@Test
public void testResizeArrayShrink() throws IOException, ZarrException {
Comment thread
brokkoli71 marked this conversation as resolved.
int[] testData = new int[10 * 10];
Arrays.setAll(testData, p -> p);

StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve("testResizeArrayShrinkV3");
ArrayMetadata arrayMetadata = Array.metadataBuilder()
.withShape(10, 10)
.withDataType(DataType.UINT32)
.withChunkShape(5, 5)
.build();
ucar.ma2.DataType ma2DataType = arrayMetadata.dataType.getMA2DataType();
Array array = Array.create(storeHandle, arrayMetadata);
array.write(new long[]{0, 0}, ucar.ma2.Array.factory(ma2DataType, new int[]{10, 10}, testData));

array = array.resize(new long[]{5, 5});
Assertions.assertArrayEquals(new int[]{5, 5}, array.read().getShape());

ucar.ma2.Array data = array.read();
int[] expectedData = new int[5 * 5];
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
expectedData[i * 5 + j] = testData[i * 10 + j];
}
}
Assertions.assertArrayEquals(expectedData, (int[]) data.get1DJavaArray(ma2DataType));
}

@Test
Expand Down
Loading