>From Hussain Towaileb <[email protected]>:
Hussain Towaileb has uploaded this change for review. (
https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21403?usp=email )
Change subject: [NO ISSUE][EXT]: Handle interrupts when reading external
collections
......................................................................
[NO ISSUE][EXT]: Handle interrupts when reading external collections
Details:
- handle interrupts when reading from external collections.
- handle invalid utf8 chars when reading from external collections.
- add property to handle invalid characters, values are:
- replace (default) to replace invalid chars with ? and continue reading
- fail to fail fast and stop reading
Ext-ref: MB-72470, MB-72724
Change-Id: Ib4c6f48470b77d3ba1e7bcc35240df468835f132
---
A asterixdb/asterix-app/data/json/illegal-character.json
M
asterixdb/asterix-app/src/test/java/org/apache/asterix/test/external_dataset/ExternalDatasetTestUtils.java
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.000.container.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.001.ddl.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.002.query.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.099.ddl.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.000.container.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.001.ddl.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.002.query.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.099.ddl.sqlpp
A
asterixdb/asterix-app/src/test/resources/runtimets/results/external-dataset/common/illegal-character/replace/result.002.adm
M
asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
M
asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
M
asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/blob/AzureBlobInputStream.java
M
asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/datalake/AzureDataLakeInputStream.java
M
asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/stream/StreamRecordReader.java
M
asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/stream/AsterixInputStreamReader.java
M
asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java
M
asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
A
asterixdb/asterix-external-data/src/test/java/org/apache/asterix/external/input/stream/AsterixInputStreamReaderIllegalCharacterTest.java
M
hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/util/ExceptionUtils.java
M
hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
22 files changed, 445 insertions(+), 7 deletions(-)
git pull ssh://asterix-gerrit.ics.uci.edu:29418/asterixdb
refs/changes/03/21403/1
diff --git a/asterixdb/asterix-app/data/json/illegal-character.json
b/asterixdb/asterix-app/data/json/illegal-character.json
new file mode 100644
index 0000000..f2dbfc1
--- /dev/null
+++ b/asterixdb/asterix-app/data/json/illegal-character.json
@@ -0,0 +1 @@
+{"id": 1, "value": "helloéworld"}
diff --git
a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/external_dataset/ExternalDatasetTestUtils.java
b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/external_dataset/ExternalDatasetTestUtils.java
index 8a94453..e4e6c70 100644
---
a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/external_dataset/ExternalDatasetTestUtils.java
+++
b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/external_dataset/ExternalDatasetTestUtils.java
@@ -45,6 +45,7 @@
import org.apache.commons.io.FilenameUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hyracks.api.util.IoUtil;
+import org.apache.hyracks.util.annotations.AiProvenance;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Assert;
@@ -75,6 +76,7 @@
public static final String PARQUET_DEFINITION = "parquet-data/reviews/";
public static final String PARQUET_NULL_TEST_DIRECTORY =
"parquet-data/null-test/";
public static final String AVRO_DEFINITION = "avro-data/reviews/";
+ public static final String ILLEGAL_CHARACTER_DEFINITION =
"illegal-character-data/";
// This is used for a test to generate over 1000 number of files
public static final String OVER_1000_OBJECTS_PATH = "over-1000-objects";
@@ -210,9 +212,21 @@
loadDeltaTableFiles();
LOGGER.info("Delta files added successfully");
+ LOGGER.info("Adding illegal-character file to the bucket");
+ loadIllegalCharacterFile();
+ LOGGER.info("Illegal-character file added successfully");
+
LOGGER.info("Files added successfully");
}
+ @AiProvenance(agent = AiProvenance.Agent.CLAUDE_SONNET_5, tool =
AiProvenance.Tool.CLAUDE_CODE_UI, contributionKind =
AiProvenance.ContributionKind.TEST_GENERATED, notes = "Uploads a file with a
single illegal (invalid UTF-8) byte, used to test the "
+ + "illegalCharacterHandling property (replace vs fail)")
+ private static void loadIllegalCharacterFile() {
+ String fileName = "illegal-character.json";
+ Path filePath = Paths.get(JSON_DATA_PATH, fileName);
+ playgroundDataLoader.upload(ILLEGAL_CHARACTER_DEFINITION + fileName,
filePath.toString(), true, false);
+ }
+
public static void prepareBrowseContainer() {
/*
file hierarchy inside browse container
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.000.container.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.000.container.sqlpp
new file mode 100644
index 0000000..068dbd3
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.000.container.sqlpp
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// create container with data
+illegal-character-fail illegal-character-data data/json/illegal-character.json
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.001.ddl.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.001.ddl.sqlpp
new file mode 100644
index 0000000..6fe8692
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.001.ddl.sqlpp
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+DROP DATAVERSE test IF EXISTS;
+CREATE DATAVERSE test;
+USE test;
+
+DROP TYPE t1 IF EXISTS;
+CREATE TYPE t1 AS {};
+
+DROP DATASET ds1 IF EXISTS;
+CREATE EXTERNAL DATASET ds1(t1) USING %adapter% (
+%template%,
+%additional_Properties%
+("container"="illegal-character-fail"),
+("definition"="illegal-character-data"),
+("format"="json"),
+("illegalCharacterHandling"="fail")
+);
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.002.query.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.002.query.sqlpp
new file mode 100644
index 0000000..3dedd2f
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.002.query.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+USE test;
+
+FROM ds1 v SELECT VALUE v;
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.099.ddl.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.099.ddl.sqlpp
new file mode 100644
index 0000000..36b2bab
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/fail/test.099.ddl.sqlpp
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+DROP DATAVERSE test IF EXISTS;
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.000.container.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.000.container.sqlpp
new file mode 100644
index 0000000..e8daf0e
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.000.container.sqlpp
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// create container with data
+illegal-character-replace illegal-character-data
data/json/illegal-character.json
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.001.ddl.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.001.ddl.sqlpp
new file mode 100644
index 0000000..5e5ecfe
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.001.ddl.sqlpp
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+DROP DATAVERSE test IF EXISTS;
+CREATE DATAVERSE test;
+USE test;
+
+DROP TYPE t1 IF EXISTS;
+CREATE TYPE t1 AS {};
+
+DROP DATASET ds1 IF EXISTS;
+CREATE EXTERNAL DATASET ds1(t1) USING %adapter% (
+%template%,
+%additional_Properties%
+("container"="illegal-character-replace"),
+("definition"="illegal-character-data"),
+("format"="json")
+);
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.002.query.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.002.query.sqlpp
new file mode 100644
index 0000000..3dedd2f
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.002.query.sqlpp
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+USE test;
+
+FROM ds1 v SELECT VALUE v;
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.099.ddl.sqlpp
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.099.ddl.sqlpp
new file mode 100644
index 0000000..36b2bab
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/illegal-character/replace/test.099.ddl.sqlpp
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+DROP DATAVERSE test IF EXISTS;
\ No newline at end of file
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/results/external-dataset/common/illegal-character/replace/result.002.adm
b/asterixdb/asterix-app/src/test/resources/runtimets/results/external-dataset/common/illegal-character/replace/result.002.adm
new file mode 100644
index 0000000..8cab9cf
--- /dev/null
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/results/external-dataset/common/illegal-character/replace/result.002.adm
@@ -0,0 +1 @@
+{ "id": 1, "value": "hello�world" }
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
index 993372e..0e5ce4d 100644
---
a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
@@ -550,6 +550,19 @@
</compilation-unit>
</test-case>
<test-case FilePath="external-dataset">
+ <compilation-unit name="common/illegal-character/replace">
+ <placeholder name="adapter" value="AZUREBLOB" />
+ <output-dir
compare="Text">common/illegal-character/replace</output-dir>
+ </compilation-unit>
+ </test-case>
+ <test-case FilePath="external-dataset">
+ <compilation-unit name="common/illegal-character/fail">
+ <placeholder name="adapter" value="AZUREBLOB" />
+ <output-dir compare="Text">common/illegal-character/fail</output-dir>
+ <expected-error>External source error</expected-error>
+ </compilation-unit>
+ </test-case>
+ <test-case FilePath="external-dataset">
<compilation-unit name="common/definition-does-not-exist">
<placeholder name="adapter" value="AZUREBLOB" />
<output-dir
compare="Text">common/definition-does-not-exist</output-dir>
diff --git
a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
index d19d5fc..28da731 100644
---
a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
+++
b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
@@ -1224,6 +1224,19 @@
</compilation-unit>
</test-case>
<test-case FilePath="external-dataset">
+ <compilation-unit name="common/illegal-character/replace">
+ <placeholder name="adapter" value="S3" />
+ <output-dir
compare="Text">common/illegal-character/replace</output-dir>
+ </compilation-unit>
+ </test-case>
+ <test-case FilePath="external-dataset">
+ <compilation-unit name="common/illegal-character/fail">
+ <placeholder name="adapter" value="S3" />
+ <output-dir compare="Text">common/illegal-character/fail</output-dir>
+ <expected-error>External source error</expected-error>
+ </compilation-unit>
+ </test-case>
+ <test-case FilePath="external-dataset">
<compilation-unit name="common/definition-does-not-exist">
<placeholder name="adapter" value="S3" />
<output-dir
compare="Text">common/definition-does-not-exist</output-dir>
diff --git
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/blob/AzureBlobInputStream.java
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/blob/AzureBlobInputStream.java
index 59e07b6..9d2e0ad 100644
---
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/blob/AzureBlobInputStream.java
+++
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/blob/AzureBlobInputStream.java
@@ -93,4 +93,14 @@
throw HyracksDataException.create(ex);
}
}
+
+ @Override
+ public boolean stop() {
+ try {
+ close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ return false;
+ }
}
diff --git
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/datalake/AzureDataLakeInputStream.java
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/datalake/AzureDataLakeInputStream.java
index 8ba3e79..cf521bb 100644
---
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/datalake/AzureDataLakeInputStream.java
+++
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/azure/datalake/AzureDataLakeInputStream.java
@@ -93,4 +93,14 @@
throw HyracksDataException.create(ex);
}
}
+
+ @Override
+ public boolean stop() {
+ try {
+ close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ return false;
+ }
}
diff --git
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/stream/StreamRecordReader.java
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/stream/StreamRecordReader.java
index 5ef63bc..15ac0b9 100644
---
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/stream/StreamRecordReader.java
+++
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/record/reader/stream/StreamRecordReader.java
@@ -43,7 +43,8 @@
protected void configure(AsterixInputStream inputStream, Map<String,
String> config) {
int bufferSize = ExternalDataUtils.getOrDefaultBufferSize(config);
- this.reader = new AsterixInputStreamReader(inputStream, bufferSize);
+ boolean failOnIllegalCharacter =
ExternalDataUtils.shouldFailOnIllegalCharacter(config);
+ this.reader = new AsterixInputStreamReader(inputStream, bufferSize,
failOnIllegalCharacter);
record = new CharArrayRecord();
inputBuffer = new char[bufferSize];
setSuppliers(config, reader::getStreamName,
reader::getPreviousStreamName);
diff --git
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/stream/AsterixInputStreamReader.java
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/stream/AsterixInputStreamReader.java
index 2dc323e..72ae5e9 100644
---
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/stream/AsterixInputStreamReader.java
+++
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/input/stream/AsterixInputStreamReader.java
@@ -18,21 +18,36 @@
*/
package org.apache.asterix.external.input.stream;
+import static org.apache.hyracks.api.util.ExceptionUtils.getMessageOrToString;
+
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.exceptions.RuntimeDataException;
import org.apache.asterix.external.api.AsterixInputStream;
import org.apache.asterix.external.dataflow.AbstractFeedDataFlowController;
import org.apache.asterix.external.util.IFeedLogManager;
import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.util.annotations.AiProvenance;
public class AsterixInputStreamReader extends Reader {
+
+ // Unicode REPLACEMENT CHARACTER (U+FFFD) -- the standard substitute for
an illegal/unmappable
+ // byte sequence during decoding (e.g. WHATWG Encoding Standard, Python's
errors='replace').
+ private static final String ILLEGAL_CHARACTER_REPLACEMENT = "\uFFFD";
+
private final AsterixInputStream in;
private final CharsetDecoder decoder;
+ private final boolean failOnIllegalCharacter;
private byte[] bytes;
protected final ByteBuffer byteBuffer;
protected final CharBuffer charBuffer;
@@ -40,14 +55,39 @@
protected boolean remaining = false;
public AsterixInputStreamReader(AsterixInputStream in, int bufferSize) {
+ this(in, bufferSize, false);
+ }
+
+ @AiProvenance(agent = AiProvenance.Agent.CLAUDE_SONNET_5, tool =
AiProvenance.Tool.CLAUDE_CODE_UI, contributionKind =
AiProvenance.ContributionKind.GENERATED, notes = "Illegal (invalid UTF-8)
characters previously stalled the decoder forever since its "
+ + "CoderResult was never consulted; default to substituting U+FFFD
and continuing, with an "
+ + "explicit fail-fast option that reports the error immediately
instead of stalling")
+ public AsterixInputStreamReader(AsterixInputStream in, int bufferSize,
boolean failOnIllegalCharacter) {
this.in = in;
+ this.failOnIllegalCharacter = failOnIllegalCharacter;
this.decoder = StandardCharsets.UTF_8.newDecoder();
+ if (!failOnIllegalCharacter) {
+
decoder.onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE)
+ .replaceWith(ILLEGAL_CHARACTER_REPLACEMENT);
+ }
this.bytes = new byte[bufferSize];
this.byteBuffer = ByteBuffer.wrap(bytes);
this.charBuffer = CharBuffer.allocate(bufferSize);
this.byteBuffer.flip();
}
+ @AiProvenance(agent = AiProvenance.Agent.CLAUDE_SONNET_5, tool =
AiProvenance.Tool.CLAUDE_CODE_UI, contributionKind =
AiProvenance.ContributionKind.GENERATED, notes = "Consult the decoder's
CoderResult (previously discarded) and, in fail-fast mode, report an "
+ + "illegal character as EXTERNAL_SOURCE_ERROR instead of the
generic untyped wrapping")
+ private void decode(boolean endOfInput) throws IOException {
+ CoderResult result = decoder.decode(byteBuffer, charBuffer,
endOfInput);
+ if (failOnIllegalCharacter && result.isError()) {
+ try {
+ result.throwException();
+ } catch (CharacterCodingException e) {
+ throw
RuntimeDataException.create(ErrorCode.EXTERNAL_SOURCE_ERROR, e,
getMessageOrToString(e));
+ }
+ }
+ }
+
public void stop() throws IOException {
try {
in.stop();
@@ -79,7 +119,7 @@
while (charBuffer.position() == 0) {
if (byteBuffer.hasRemaining()) {
remaining = true;
- decoder.decode(byteBuffer, charBuffer, false);
+ decode(false);
System.arraycopy(charBuffer.array(), 0, cbuf, offset,
charBuffer.position());
if (charBuffer.position() > 0) {
return charBuffer.position();
@@ -90,12 +130,14 @@
byteBuffer.position(byteBuffer.remaining());
byteBuffer.limit(byteBuffer.capacity()); //set limit to
capacity for the new bytes
while (len == 0) {
+ checkInterrupted();
len = in.read(bytes, byteBuffer.position(),
bytes.length - byteBuffer.position());
}
}
} else {
byteBuffer.clear();
while (len == 0) {
+ checkInterrupted();
len = in.read(bytes, 0, bytes.length);
}
}
@@ -110,12 +152,20 @@
}
byteBuffer.flip();
remaining = false;
- decoder.decode(byteBuffer, charBuffer, false);
+ decode(false);
System.arraycopy(charBuffer.array(), 0, cbuf, offset,
charBuffer.position());
}
return charBuffer.position();
}
+ @AiProvenance(agent = AiProvenance.Agent.DEEPSEEK_CODER, tool =
AiProvenance.Tool.FACTORY_CLI, contributionKind =
AiProvenance.ContributionKind.ASSISTED, notes = "Check for thread interruption
(re-interrupts thread and throws InterruptedIOException) to prevent infinite
spin in read loop when stream is cancelled")
+ private static void checkInterrupted() throws IOException {
+ if (Thread.interrupted()) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException("Thread interrupted while reading
stream");
+ }
+ }
+
@Override
public void close() throws IOException {
in.close();
diff --git
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java
index 8f0d363..fbdfb91 100644
---
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java
+++
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataConstants.java
@@ -47,6 +47,10 @@
public static final String KEY_DATASET_DATAVERSE = "dataset-dataverse";
// used to specify the socket addresses when reading data from sockets
public static final String KEY_SOCKETS = "sockets";
+ // specify how an illegal (invalid UTF-8) character in the source should
be handled
+ public static final String KEY_ILLEGAL_CHARACTER_HANDLING =
"illegalCharacterHandling";
+ // fail the read as soon as an illegal character is encountered
+ public static final String ILLEGAL_CHARACTER_HANDLING_FAIL = "fail";
// specify whether the socket address points to an NC or an IP
public static final String KEY_MODE = "address-type";
// specify the HDFS name node address when reading HDFS data
diff --git
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
index 713a1fb..b405c88 100644
---
a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
+++
b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
@@ -31,10 +31,12 @@
import static org.apache.asterix.common.utils.CSVConstants.KEY_QUOTE;
import static
org.apache.asterix.external.util.ExternalDataConstants.DEFINITION_FIELD_NAME;
import static
org.apache.asterix.external.util.ExternalDataConstants.DISABLE_SSL_VERIFY_FIELD_NAME;
+import static
org.apache.asterix.external.util.ExternalDataConstants.ILLEGAL_CHARACTER_HANDLING_FAIL;
import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_ADAPTER_NAME_AZURE_DATALAKE;
import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_ADAPTER_NAME_AZURE_DATALAKE_ALIAS;
import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_EXCLUDE;
import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_EXTERNAL_SCAN_BUFFER_SIZE;
+import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_ILLEGAL_CHARACTER_HANDLING;
import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_INCLUDE;
import static org.apache.asterix.external.util.ExternalDataConstants.KEY_PATH;
import static
org.apache.asterix.external.util.ExternalDataConstants.KEY_RECORD_END;
@@ -173,6 +175,12 @@
return bufferSize != null ? Integer.parseInt(bufferSize) :
ExternalDataConstants.DEFAULT_BUFFER_SIZE;
}
+ // Defaults to replace-and-continue; only "fail" opts into failing fast on
an illegal character.
+ public static boolean shouldFailOnIllegalCharacter(Map<String, String>
configuration) {
+ String mode = configuration.get(KEY_ILLEGAL_CHARACTER_HANDLING);
+ return ILLEGAL_CHARACTER_HANDLING_FAIL.equalsIgnoreCase(mode);
+ }
+
// Get a delimiter from the given configuration
public static char validateGetDelimiter(Map<String, String> configuration)
throws HyracksDataException {
return validateCharOrDefault(configuration, KEY_DELIMITER,
ExternalDataConstants.DEFAULT_DELIMITER.charAt(0));
diff --git
a/asterixdb/asterix-external-data/src/test/java/org/apache/asterix/external/input/stream/AsterixInputStreamReaderIllegalCharacterTest.java
b/asterixdb/asterix-external-data/src/test/java/org/apache/asterix/external/input/stream/AsterixInputStreamReaderIllegalCharacterTest.java
new file mode 100644
index 0000000..b60c47a
--- /dev/null
+++
b/asterixdb/asterix-external-data/src/test/java/org/apache/asterix/external/input/stream/AsterixInputStreamReaderIllegalCharacterTest.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.asterix.external.input.stream;
+
+import java.io.IOException;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.external.util.ExternalDataConstants;
+import org.apache.asterix.external.util.FileSystemWatcher;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+import org.apache.hyracks.util.annotations.AiProvenance;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Regression test for AsterixInputStreamReader's handling of an illegal
+ * (invalid UTF-8) character byte in the source: previously, CharsetDecoder's
+ * CoderResult was never consulted, so the decoder would permanently stall at
+ * the bad byte -- never skipping or reporting past it -- until the internal
+ * byte buffer saturated and every subsequent read became a non-blocking,
+ * CPU-pegging infinite spin. The fix configures the decoder to substitute
+ * illegal characters with the Unicode REPLACEMENT CHARACTER (U+FFFD) and
+ * keep reading by default, with an explicit fail-fast option that reports
+ * the error immediately as an EXTERNAL_SOURCE_ERROR instead of stalling.
+ */
+@AiProvenance(agent = AiProvenance.Agent.CLAUDE_SONNET_5, tool =
AiProvenance.Tool.CLAUDE_CODE_UI, contributionKind =
AiProvenance.ContributionKind.TEST_GENERATED, notes = "Regression test for
illegal-character handling (replace-by-default, fail-fast option) in "
+ + "AsterixInputStreamReader")
+public class AsterixInputStreamReaderIllegalCharacterTest {
+
+ private Path tempFile;
+
+ @After
+ public void cleanup() throws IOException {
+ if (tempFile != null) {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ private Path writeIllegalCharacterFile() throws IOException {
+ Path file = Files.createTempFile("illegal-character-utf8", ".json");
+ byte[] prefix = "{\"value\": \"".getBytes(StandardCharsets.US_ASCII);
+ byte[] illegalCharacter = { (byte) 0xE9 };
+ byte[] suffix = "hello\"}\n".getBytes(StandardCharsets.US_ASCII);
+ try (java.io.OutputStream out = Files.newOutputStream(file)) {
+ out.write(prefix);
+ out.write(illegalCharacter);
+ out.write(suffix);
+ }
+ return file;
+ }
+
+ private AsterixInputStreamReader createReader(Path file, boolean
failOnIllegalCharacter) throws Exception {
+ FileSystemWatcher watcher = new
FileSystemWatcher(Collections.singletonList(file), null, false);
+ LocalFSInputStream in = new LocalFSInputStream(watcher);
+ return new AsterixInputStreamReader(in,
ExternalDataConstants.DEFAULT_BUFFER_SIZE, failOnIllegalCharacter);
+ }
+
+ @Test(timeout = 15000)
+ public void testDefaultReplacesIllegalCharacterAndContinues() throws
Exception {
+ tempFile = writeIllegalCharacterFile();
+ AsterixInputStreamReader reader = createReader(tempFile, false);
+
+ char[] buf = new char[ExternalDataConstants.DEFAULT_BUFFER_SIZE];
+ int len = reader.read(buf);
+
+ Assert.assertEquals("{\"value\": \"\uFFFDhello\"}\n", new String(buf,
0, len));
+ }
+
+ @Test(timeout = 15000)
+ public void testFailOnIllegalCharacterThrowsImmediately() throws Exception
{
+ tempFile = writeIllegalCharacterFile();
+ AsterixInputStreamReader reader = createReader(tempFile, true);
+
+ try {
+ reader.read(new char[ExternalDataConstants.DEFAULT_BUFFER_SIZE]);
+ Assert.fail("expected an EXTERNAL_SOURCE_ERROR for the illegal
character");
+ } catch (HyracksDataException e) {
+ // reported immediately as a typed EXTERNAL_SOURCE_ERROR, not
stalled
+ Assert.assertTrue("expected EXTERNAL_SOURCE_ERROR, got: " + e,
e.matches(ErrorCode.EXTERNAL_SOURCE_ERROR));
+ Assert.assertTrue("expected the cause to be a
CharacterCodingException, got: " + e.getCause(),
+ e.getCause() instanceof CharacterCodingException);
+ }
+ }
+
+ @Test
+ public void testWellFormedFileReadsNormally() throws Exception {
+ tempFile = Files.createTempFile("well-formed-utf8", ".json");
+ String content = "{\"value\": \"hello world\"}\n";
+ Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8));
+
+ AsterixInputStreamReader reader = createReader(tempFile, false);
+ char[] buf = new char[ExternalDataConstants.DEFAULT_BUFFER_SIZE];
+ int len = reader.read(buf);
+
+ Assert.assertEquals(content, new String(buf, 0, len));
+ }
+}
diff --git
a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/util/ExceptionUtils.java
b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/util/ExceptionUtils.java
index 81de6ed..17f7827 100644
---
a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/util/ExceptionUtils.java
+++
b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/util/ExceptionUtils.java
@@ -18,6 +18,7 @@
*/
package org.apache.hyracks.api.util;
+import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -86,7 +87,7 @@
private static boolean possibleRootCause(Throwable exception) {
Throwable cause = exception;
while ((cause = cause.getCause()) != null) {
- if (cause instanceof java.lang.InterruptedException
+ if (cause instanceof java.lang.InterruptedException || cause
instanceof InterruptedIOException
|| cause instanceof
java.nio.channels.ClosedChannelException) {
return false;
}
@@ -105,7 +106,7 @@
* @return the root exception, or null if both parameters are null
*/
public static <T extends Throwable> T suppress(T first, T second) {
- if (second instanceof InterruptedException) {
+ if (second instanceof InterruptedException || second instanceof
InterruptedIOException) {
Thread.currentThread().interrupt();
}
if (first == null) {
@@ -149,10 +150,11 @@
}
public static boolean causedByInterrupt(Throwable th, boolean
skipInterruptedCheck) {
- if (th instanceof InterruptedException) {
+ if (th instanceof InterruptedException || th instanceof
InterruptedIOException) {
return true;
}
- boolean isCausedByInterrupt = getRootCause(th) instanceof
InterruptedException;
+ boolean isCausedByInterrupt =
+ getRootCause(th) instanceof InterruptedException ||
getRootCause(th) instanceof InterruptedIOException;
if (!skipInterruptedCheck && isCausedByInterrupt &&
!Thread.currentThread().isInterrupted()) {
LOGGER.warn("InterruptedException suppressed and
!Thread.currentThread().isInterrupted()", th);
}
diff --git
a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
b/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
index f20956d..2afaafd 100644
---
a/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
+++
b/hyracks-fullstack/hyracks/hyracks-util/src/main/java/org/apache/hyracks/util/annotations/AiProvenance.java
@@ -203,6 +203,7 @@
// =========================
// Anthropic â Claude Sonnet
// =========================
+ CLAUDE_SONNET_5(ANTHROPIC, "claude-sonnet-5", "Claude Sonnet 5"),
CLAUDE_SONNET_4(ANTHROPIC, "claude-4-sonnet", "Claude Sonnet 4"),
CLAUDE_SONNET_4_5(ANTHROPIC, "claude-4-sonnet-4.5", "Claude Sonnet
4.5"),
CLAUDE_SONNET_4_6(ANTHROPIC, "claude-4-sonnet-4.6", "Claude Sonnet
4.6"),
--
To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21403?usp=email
To unsubscribe, or for help writing mail filters, visit
https://asterix-gerrit.ics.uci.edu/settings?usp=email
Gerrit-MessageType: newchange
Gerrit-Project: asterixdb
Gerrit-Branch: master
Gerrit-Change-Id: Ib4c6f48470b77d3ba1e7bcc35240df468835f132
Gerrit-Change-Number: 21403
Gerrit-PatchSet: 1
Gerrit-Owner: Hussain Towaileb <[email protected]>