Author: tomwhite
Date: Fri Nov 9 10:56:27 2012
New Revision: 1407419
URL: http://svn.apache.org/viewvc?rev=1407419&view=rev
Log:
AVRO-1200. DatumWriter can write malformed data if collection is modified
during write.
Modified:
avro/trunk/CHANGES.txt
avro/trunk/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
avro/trunk/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java
Modified: avro/trunk/CHANGES.txt
URL:
http://svn.apache.org/viewvc/avro/trunk/CHANGES.txt?rev=1407419&r1=1407418&r2=1407419&view=diff
==============================================================================
--- avro/trunk/CHANGES.txt (original)
+++ avro/trunk/CHANGES.txt Fri Nov 9 10:56:27 2012
@@ -21,7 +21,7 @@ Trunk (not yet released)
(Hernan Otero via cutting)
AVRO-983. maven-avro-plugin: Allow maven schema compiler to support
- external reference in an another avro schema file (tomwhite)
+ external reference in an another avro schema file. (tomwhite)
BUG FIXES
@@ -34,6 +34,9 @@ Trunk (not yet released)
AVRO-1197. Java: Expose mapreduce tests so that 'maven install'
works correctly. (Mike Percy via cutting)
+ AVRO-1200. DatumWriter can write malformed data if collection is
+ modified during write. (tomwhite)
+
Avro 1.7.2 (20 October 2012)
Modified:
avro/trunk/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
URL:
http://svn.apache.org/viewvc/avro/trunk/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java?rev=1407419&r1=1407418&r2=1407419&view=diff
==============================================================================
---
avro/trunk/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
(original)
+++
avro/trunk/lang/java/avro/src/main/java/org/apache/avro/generic/GenericDatumWriter.java
Fri Nov 9 10:56:27 2012
@@ -19,6 +19,7 @@ package org.apache.avro.generic;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Map;
import java.util.Collection;
@@ -122,13 +123,19 @@ public class GenericDatumWriter<D> imple
throws IOException {
Schema element = schema.getElementType();
long size = getArraySize(datum);
+ long actualSize = 0;
out.writeArrayStart();
out.setItemCount(size);
for (Iterator<? extends Object> it = getArrayElements(datum);
it.hasNext();) {
out.startItem();
write(element, it.next(), out);
+ actualSize++;
}
out.writeArrayEnd();
+ if (actualSize != size) {
+ throw new ConcurrentModificationException("Size of array written was " +
+ size + ", but number of elements written was " + actualSize + ". ");
+ }
}
/** Called to find the index for a datum within a union. By default calls
@@ -157,14 +164,20 @@ public class GenericDatumWriter<D> imple
throws IOException {
Schema value = schema.getValueType();
int size = getMapSize(datum);
+ int actualSize = 0;
out.writeMapStart();
out.setItemCount(size);
for (Map.Entry<Object,Object> entry : getMapEntries(datum)) {
out.startItem();
writeString(entry.getKey().toString(), out);
write(value, entry.getValue(), out);
+ actualSize++;
}
out.writeMapEnd();
+ if (actualSize != size) {
+ throw new ConcurrentModificationException("Size of map written was " +
+ size + ", but number of entries written was " + actualSize + ". ");
+ }
}
/** Called by the default implementation of {@link #writeMap} to get the size
Modified:
avro/trunk/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java
URL:
http://svn.apache.org/viewvc/avro/trunk/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java?rev=1407419&r1=1407418&r2=1407419&view=diff
==============================================================================
---
avro/trunk/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java
(original)
+++
avro/trunk/lang/java/avro/src/test/java/org/apache/avro/generic/TestGenericDatumWriter.java
Fri Nov 9 10:56:27 2012
@@ -18,17 +18,32 @@
package org.apache.avro.generic;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import org.apache.avro.Schema;
import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.DirectBinaryEncoder;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.io.JsonDecoder;
import org.junit.Test;
+import org.apache.avro.util.Utf8;
public class TestGenericDatumWriter {
@Test
@@ -50,4 +65,150 @@ public class TestGenericDatumWriter {
DecoderFactory.get().jsonDecoder(s, new
ByteArrayInputStream(bao.toByteArray())));
assertEquals(r, o);
}
+
+ @Test
+ public void testArrayConcurrentModification() throws Exception {
+ String json = "{\"type\": \"array\", \"items\": \"int\" }";
+ Schema s = Schema.parse(json);
+ final GenericArray<Integer> a = new GenericData.Array<Integer>(1, s);
+ ByteArrayOutputStream bao = new ByteArrayOutputStream();
+ final GenericDatumWriter<GenericArray<Integer>> w =
+ new GenericDatumWriter<GenericArray<Integer>>(s);
+
+ CountDownLatch sizeWrittenSignal = new CountDownLatch(1);
+ CountDownLatch eltAddedSignal = new CountDownLatch(1);
+
+ final TestEncoder e = new TestEncoder(EncoderFactory.get()
+ .directBinaryEncoder(bao, null), sizeWrittenSignal, eltAddedSignal);
+
+ // call write in another thread
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<Void> result = executor.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ w.write(a, e);
+ return null;
+ }
+ });
+ sizeWrittenSignal.await();
+ // size has been written so now add an element to the array
+ a.add(7);
+ // and signal for the element to be written
+ eltAddedSignal.countDown();
+ try {
+ result.get();
+ fail("Expected ConcurrentModificationException");
+ } catch (ExecutionException ex) {
+ assertTrue(ex.getCause() instanceof ConcurrentModificationException);
+ }
+ }
+
+
+ @Test
+ public void testMapConcurrentModification() throws Exception {
+ String json = "{\"type\": \"map\", \"values\": \"int\" }";
+ Schema s = Schema.parse(json);
+ final Map<String, Integer> m = new HashMap<String, Integer>();
+ ByteArrayOutputStream bao = new ByteArrayOutputStream();
+ final GenericDatumWriter<Map<String, Integer>> w =
+ new GenericDatumWriter<Map<String, Integer>>(s);
+
+ CountDownLatch sizeWrittenSignal = new CountDownLatch(1);
+ CountDownLatch eltAddedSignal = new CountDownLatch(1);
+
+ final TestEncoder e = new TestEncoder(EncoderFactory.get()
+ .directBinaryEncoder(bao, null), sizeWrittenSignal, eltAddedSignal);
+
+ // call write in another thread
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<Void> result = executor.submit(new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ w.write(m, e);
+ return null;
+ }
+ });
+ sizeWrittenSignal.await();
+ // size has been written so now add an entry to the map
+ m.put("a", 7);
+ // and signal for the entry to be written
+ eltAddedSignal.countDown();
+ try {
+ result.get();
+ fail("Expected ConcurrentModificationException");
+ } catch (ExecutionException ex) {
+ assertTrue(ex.getCause() instanceof ConcurrentModificationException);
+ }
+ }
+
+ static class TestEncoder extends Encoder {
+
+ Encoder e;
+ CountDownLatch sizeWrittenSignal;
+ CountDownLatch eltAddedSignal;
+
+ TestEncoder(Encoder encoder, CountDownLatch sizeWrittenSignal,
+ CountDownLatch eltAddedSignal) {
+ this.e = encoder;
+ this.sizeWrittenSignal = sizeWrittenSignal;
+ this.eltAddedSignal = eltAddedSignal;
+ }
+
+ @Override
+ public void writeArrayStart() throws IOException {
+ e.writeArrayStart();
+ sizeWrittenSignal.countDown();
+ try {
+ eltAddedSignal.await();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void writeMapStart() throws IOException {
+ e.writeMapStart();
+ sizeWrittenSignal.countDown();
+ try {
+ eltAddedSignal.await();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void flush() throws IOException { e.flush(); }
+ @Override
+ public void writeNull() throws IOException { e.writeNull(); }
+ @Override
+ public void writeBoolean(boolean b) throws IOException {
e.writeBoolean(b); }
+ @Override
+ public void writeInt(int n) throws IOException { e.writeInt(n); }
+ @Override
+ public void writeLong(long n) throws IOException { e.writeLong(n); }
+ @Override
+ public void writeFloat(float f) throws IOException { e.writeFloat(f); }
+ @Override
+ public void writeDouble(double d) throws IOException { e.writeDouble(d); }
+ @Override
+ public void writeString(Utf8 utf8) throws IOException {
e.writeString(utf8); }
+ @Override
+ public void writeBytes(ByteBuffer bytes) throws IOException {
e.writeBytes(bytes); }
+ @Override
+ public void writeBytes(byte[] bytes, int start, int len) throws
IOException { e.writeBytes(bytes, start, len); }
+ @Override
+ public void writeFixed(byte[] bytes, int start, int len) throws
IOException { e.writeFixed(bytes, start, len); }
+ @Override
+ public void writeEnum(int en) throws IOException { e.writeEnum(en); }
+ @Override
+ public void setItemCount(long itemCount) throws IOException {
e.setItemCount(itemCount); }
+ @Override
+ public void startItem() throws IOException { e.startItem(); }
+ @Override
+ public void writeArrayEnd() throws IOException { e.writeArrayEnd(); }
+ @Override
+ public void writeMapEnd() throws IOException { e.writeMapEnd(); }
+ @Override
+ public void writeIndex(int unionIndex) throws IOException {
e.writeIndex(unionIndex); }
+ };
}