From d87484b9b2eb2395499a19b59be41866c223b781 Mon Sep 17 00:00:00 2001 From: Hubert Plociniczak Date: Fri, 20 Dec 2024 15:42:56 +0100 Subject: [PATCH] Do not report exceptions on long running Excel reads (#11916) * Do not report exceptions on long running Excel reads This change introduces two modifications: - `ClosedByInterruptException` is wrapped in `InterruptedException` instead of `RuntimeException` - when instrumentation encounters `InterruptedException` it bails early Having `ClosedByInterruptException` wrapped in `RuntimeException` meant that it is being reported as a regular `HostException` in the engine and to the user. Instead it should be treated specially since we know that it is caused by cancelling a long-running job. Since it is a statically checked exception it has to be declared and the information has to be propagated through various lambda constructs (thanks Java!). The above change alone meant that an error is not reported for `Data.read` nodes but any values dependent on it would still report `No_Such_Method` error when the exception is reported as a value. Hence the early bail out mechanism. * Send `PendingInterrupted` on interrupt The information could be used in GUI to indicate pending execution that will take tad longer. * Prettify * Test `PendingInterrupted` payload * Add `wasInterrupted` flag to `Pending` Reduce `PendingInterrupted` to a flag in `Pending` * fmt --- .../protocol-language-server.md | 4 +- .../runtime/ContextEventsListener.scala | 4 +- .../runtime/ContextRegistryProtocol.scala | 26 +++++++- .../org/enso/polyglot/runtime/Runtime.scala | 9 ++- .../job/ProgramExecutionSupport.scala | 59 +++++++++++++++++++ .../instrument/RuntimeAsyncCommandsTest.scala | 7 +-- .../test/instrument/TestMessages.scala | 29 +++++++++ .../control/ThreadInterruptedException.java | 8 ++- .../enso/table/excel/ExcelConnectionPool.java | 10 ++-- .../java/org/enso/table/excel/ExcelRange.java | 5 +- .../java/org/enso/table/excel/ExcelSheet.java | 6 +- .../table/excel/ReadOnlyExcelConnection.java | 6 +- .../excel/xssfreader/XSSFReaderSheet.java | 13 ++-- .../excel/xssfreader/XSSFReaderWorkbook.java | 20 ++++--- .../java/org/enso/table/read/ExcelReader.java | 28 +++++---- .../table/util/ConsumerWithException.java | 45 ++++++++++++++ .../table/util/FunctionWithException.java | 51 ++++++++++++++++ .../org/enso/table/write/ExcelWriter.java | 25 +++++--- 18 files changed, 299 insertions(+), 56 deletions(-) create mode 100644 std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java create mode 100644 std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 7006d840adf..e47567953a2 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -392,7 +392,7 @@ interface ExpressionUpdate { An information about the computed value. ```typescript -type ExpressionUpdatePayload = Value | DatafalowError | Panic | Pending; +type ExpressionUpdatePayload = Value | DataflowError | Panic | Pending; /** Indicates that the expression was computed to a value. */ interface Value { @@ -424,6 +424,8 @@ interface Pending { /** Optional amount of already done work as a number between `0.0` to `1.0`. */ progress?: number; + /** Indicates whether the computation of the expression has been interrupted and will be retried. */ + wasInterrupted: boolean; } /** Information about warnings associated with the value. */ diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala index 154a4328a8e..e6596b45033 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala @@ -230,8 +230,8 @@ final class ContextEventsListener( functionSchema.map(toProtocolFunctionSchema) ) - case Api.ExpressionUpdate.Payload.Pending(m, p) => - ContextRegistryProtocol.ExpressionUpdate.Payload.Pending(m, p) + case Api.ExpressionUpdate.Payload.Pending(m, p, i) => + ContextRegistryProtocol.ExpressionUpdate.Payload.Pending(m, p, i) case Api.ExpressionUpdate.Payload.DataflowError(trace) => ContextRegistryProtocol.ExpressionUpdate.Payload.DataflowError(trace) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala index 29e083f5069..aef9df5d316 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala @@ -231,8 +231,17 @@ object ContextRegistryProtocol { ) } - case class Pending(message: Option[String], progress: Option[Double]) - extends Payload + /** Indicates that an expression is pending a computation + */ + case class Pending( + message: Option[String], + progress: Option[Double], + wasInterrupted: Boolean + ) extends Payload + + /** Indicates that an expression's computation has been interrupted and shall be retried. + */ + case object PendingInterrupted extends Payload /** Indicates that the expression was computed to an error. * @@ -258,6 +267,8 @@ object ContextRegistryProtocol { val Pending = "Pending" + val PendingInterrupted = "PendingInterrupted" + val DataflowError = "DataflowError" val Panic = "Panic" @@ -291,6 +302,14 @@ object ContextRegistryProtocol { .deepMerge( Json.obj(CodecField.Type -> PayloadType.Pending.asJson) ) + case m: Payload.PendingInterrupted.type => + Encoder[Payload.PendingInterrupted.type] + .apply(m) + .deepMerge( + Json.obj( + CodecField.Type -> PayloadType.PendingInterrupted.asJson + ) + ) } implicit val decoder: Decoder[Payload] = @@ -307,6 +326,9 @@ object ContextRegistryProtocol { case PayloadType.Pending => Decoder[Payload.Pending].tryDecode(cursor) + + case PayloadType.PendingInterrupted => + Decoder[Payload.PendingInterrupted.type].tryDecode(cursor) } } } diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index 9d9ab4ccab9..cec87900947 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -158,11 +158,14 @@ object Runtime { ) } - /** TBD + /** Indicates that an expression is pending a computation */ @named("expressionUpdatePayloadPending") - case class Pending(message: Option[String], progress: Option[Double]) - extends Payload; + case class Pending( + message: Option[String], + progress: Option[Double], + wasInterrupted: Boolean = false + ) extends Payload /** Indicates that the expression was computed to an error. * diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala index 283ed9c2893..53d89c8eaa7 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala @@ -95,6 +95,21 @@ object ProgramExecutionSupport { val onComputedValueCallback: Consumer[ExpressionValue] = { value => if (callStack.isEmpty) { logger.log(Level.FINEST, s"ON_COMPUTED ${value.getExpressionId}") + + if (VisualizationResult.isInterruptedException(value.getValue)) { + value.getValue match { + case e: AbstractTruffleException => + sendInterruptedExpressionUpdate( + contextId, + executionFrame.syncState, + value + ) + // Bail out early. Any references to this value that do not expect + // Interrupted error will likely return `No_Such_Method` otherwise. + throw new ThreadInterruptedException(e); + case _ => + } + } sendExpressionUpdate(contextId, executionFrame.syncState, value) sendVisualizationUpdates( contextId, @@ -377,6 +392,50 @@ object ProgramExecutionSupport { Api.ExecutionResult.Failure(ex.getMessage, None) } + private def sendInterruptedExpressionUpdate( + contextId: ContextId, + syncState: UpdatesSynchronizationState, + value: ExpressionValue + )(implicit ctx: RuntimeContext): Unit = { + val expressionId = value.getExpressionId + val methodCall = toMethodCall(value) + if ( + !syncState.isExpressionSync(expressionId) || + (methodCall.isDefined && !syncState.isMethodPointerSync( + expressionId + )) + ) { + val payload = + Api.ExpressionUpdate.Payload.Pending(None, None, wasInterrupted = true) + ctx.endpoint.sendToClient( + Api.Response( + Api.ExpressionUpdates( + contextId, + Set( + Api.ExpressionUpdate( + value.getExpressionId, + Option(value.getTypes).map(_.toVector), + methodCall, + value.getProfilingInfo.map { case e: ExecutionTime => + Api.ProfilingInfo.ExecutionTime(e.getNanoTimeElapsed) + }.toVector, + value.wasCached(), + value.isTypeChanged || value.isFunctionCallChanged, + payload + ) + ) + ) + ) + ) + + syncState.setExpressionSync(expressionId) + ctx.state.expressionExecutionState.setExpressionExecuted(expressionId) + if (methodCall.isDefined) { + syncState.setMethodPointerSync(expressionId) + } + } + } + private def sendExpressionUpdate( contextId: ContextId, syncState: UpdatesSynchronizationState, diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala index 59d323a64a3..a86699d67d8 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeAsyncCommandsTest.scala @@ -500,16 +500,15 @@ class RuntimeAsyncCommandsTest responses should contain theSameElementsAs Seq( Api.Response(requestId, Api.RecomputeContextResponse(contextId)), - TestMessages.update( + TestMessages.pendingInterrupted( contextId, - vId, - ConstantsGen.INTEGER, methodCall = Some( MethodCall( MethodPointer("Enso_Test.Test.Main", "Enso_Test.Test.Main", "loop"), Vector(1) ) - ) + ), + vId ), context.executionComplete(contextId) ) diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala index e0dd0228742..b997f500651 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/TestMessages.scala @@ -479,4 +479,33 @@ object TestMessages { ) ) + /** Create an pending interrupted response. + * + * @param contextId an identifier of the context + * @param expressionIds a list of pending expressions + * @return the expression update response + */ + def pendingInterrupted( + contextId: UUID, + methodCall: Option[Api.MethodCall], + expressionIds: UUID* + ): Api.Response = + Api.Response( + Api.ExpressionUpdates( + contextId, + expressionIds.toSet.map { expressionId => + Api.ExpressionUpdate( + expressionId, + None, + methodCall, + Vector(Api.ProfilingInfo.ExecutionTime(0)), + false, + true, + Api.ExpressionUpdate.Payload + .Pending(None, None, wasInterrupted = true) + ) + } + ) + ) + } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java index 2693b6ee141..cd004b80060 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/control/ThreadInterruptedException.java @@ -1,4 +1,10 @@ package org.enso.interpreter.runtime.control; /** Thrown when guest code discovers a thread interrupt. */ -public class ThreadInterruptedException extends RuntimeException {} +public class ThreadInterruptedException extends RuntimeException { + public ThreadInterruptedException() {} + + public ThreadInterruptedException(Throwable e) { + super(e); + } +} diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java b/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java index af8a43263ac..5221576f113 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ExcelConnectionPool.java @@ -22,6 +22,7 @@ import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.enso.base.cache.ReloadDetector; import org.enso.table.excel.xssfreader.XSSFReaderWorkbook; +import org.enso.table.util.FunctionWithException; public class ExcelConnectionPool { public static final ExcelConnectionPool INSTANCE = new ExcelConnectionPool(); @@ -29,7 +30,7 @@ public class ExcelConnectionPool { private ExcelConnectionPool() {} public ReadOnlyExcelConnection openReadOnlyConnection(File file, ExcelFileFormat format) - throws IOException { + throws IOException, InterruptedException { synchronized (this) { if (isCurrentlyWriting) { throw new IllegalStateException( @@ -134,7 +135,7 @@ public class ExcelConnectionPool { */ public R lockForWriting( File file, ExcelFileFormat format, File[] accompanyingFiles, Function action) - throws IOException { + throws IOException, InterruptedException { synchronized (this) { if (isCurrentlyWriting) { throw new IllegalStateException( @@ -242,7 +243,8 @@ public class ExcelConnectionPool { private ExcelWorkbook workbook; private IOException initializationException = null; - T withWorkbook(Function action) throws IOException { + T withWorkbook(FunctionWithException action) + throws IOException, InterruptedException { synchronized (this) { return action.apply(accessCurrentWorkbook()); } @@ -258,7 +260,7 @@ public class ExcelConnectionPool { } } - void reopen(boolean throwOnFailure) throws IOException { + void reopen(boolean throwOnFailure) throws IOException, InterruptedException { synchronized (this) { if (workbook != null) { throw new IllegalStateException("The workbook is already open."); diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java b/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java index 552f3385fd2..4d6526829d1 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ExcelRange.java @@ -181,7 +181,8 @@ public class ExcelRange { * @param sheet ExcelSheet containing the range refers to. * @return Expanded range covering the connected table of cells. */ - public static ExcelRange expandSingleCell(ExcelRange excelRange, ExcelSheet sheet) { + public static ExcelRange expandSingleCell(ExcelRange excelRange, ExcelSheet sheet) + throws InterruptedException { ExcelRow currentRow = sheet.get(excelRange.getTopRow()); if (currentRow == null || currentRow.isEmpty(excelRange.getLeftColumn())) { return new ExcelRange( @@ -337,7 +338,7 @@ public class ExcelRange { return isWholeColumn() ? Integer.MAX_VALUE : bottomRow - topRow + 1; } - public int getLastNonEmptyRow(ExcelSheet sheet) { + public int getLastNonEmptyRow(ExcelSheet sheet) throws InterruptedException { int lastRow = Math.min(sheet.getLastRow(), isWholeColumn() ? sheet.getLastRow() : bottomRow) + 1; diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java b/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java index 4d2dd42a2a3..4dbe433d5d6 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ExcelSheet.java @@ -12,10 +12,10 @@ public interface ExcelSheet { String getName(); /** Gets the initial row index within the sheet (1-based). */ - int getFirstRow(); + int getFirstRow() throws InterruptedException; /** Gets the final row index within the sheet (1-based). */ - int getLastRow(); + int getLastRow() throws InterruptedException; /** * Gets the row at the given index within the sheet (1-based) @@ -23,7 +23,7 @@ public interface ExcelSheet { * @param row the row index (1-based)/ * @return the row object or null if the row index is out of range or doesn't exist. */ - ExcelRow get(int row); + ExcelRow get(int row) throws InterruptedException; /** Gets the underlying Apache POI Sheet object - may be null. Provided for Writer use only. */ Sheet getSheet(); diff --git a/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java b/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java index 3cbac859648..894771c9667 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/ReadOnlyExcelConnection.java @@ -1,7 +1,7 @@ package org.enso.table.excel; import java.io.IOException; -import java.util.function.Function; +import org.enso.table.util.FunctionWithException; public class ReadOnlyExcelConnection implements AutoCloseable { @@ -27,7 +27,9 @@ public class ReadOnlyExcelConnection implements AutoCloseable { record = null; } - public synchronized T withWorkbook(Function f) throws IOException { + public synchronized T withWorkbook( + FunctionWithException f) + throws IOException, InterruptedException { if (record == null) { throw new IllegalStateException("ReadOnlyExcelConnection is being used after it was closed."); } diff --git a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java index cdb79cbdbd5..2fc288ab071 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderSheet.java @@ -1,6 +1,7 @@ package org.enso.table.excel.xssfreader; import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; @@ -33,7 +34,7 @@ public class XSSFReaderSheet implements ExcelSheet { this.parent = parent; } - private synchronized void ensureReadSheetData() { + private synchronized void ensureReadSheetData() throws InterruptedException { if (hasReadSheetData) { return; } @@ -70,6 +71,8 @@ public class XSSFReaderSheet implements ExcelSheet { try { var sheet = reader.getSheet(relId); xmlReader.parse(new InputSource(sheet)); + } catch (ClosedByInterruptException e) { + throw new InterruptedException(e.getMessage()); } catch (SAXException | InvalidFormatException | IOException e) { throw new RuntimeException(e); } @@ -94,25 +97,25 @@ public class XSSFReaderSheet implements ExcelSheet { return sheetName; } - public String getDimensions() { + public String getDimensions() throws InterruptedException { ensureReadSheetData(); return dimensions; } @Override - public int getFirstRow() { + public int getFirstRow() throws InterruptedException { ensureReadSheetData(); return firstRow; } @Override - public int getLastRow() { + public int getLastRow() throws InterruptedException { ensureReadSheetData(); return lastRow; } @Override - public ExcelRow get(int row) { + public ExcelRow get(int row) throws InterruptedException { ensureReadSheetData(); if (!rowData.containsKey(row)) { diff --git a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java index 6502057ff41..46f1d441914 100644 --- a/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java +++ b/std-bits/table/src/main/java/org/enso/table/excel/xssfreader/XSSFReaderWorkbook.java @@ -1,6 +1,7 @@ package org.enso.table.excel.xssfreader; import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -8,7 +9,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPathConstants; @@ -26,6 +26,7 @@ import org.apache.poi.xssf.model.SharedStrings; import org.apache.poi.xssf.usermodel.XSSFRelation; import org.enso.table.excel.ExcelSheet; import org.enso.table.excel.ExcelWorkbook; +import org.enso.table.util.ConsumerWithException; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -90,7 +91,7 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { private SharedStrings sharedStrings; private XSSFReaderFormats styles; - public XSSFReaderWorkbook(String path) throws IOException { + public XSSFReaderWorkbook(String path) throws IOException, InterruptedException { this.path = path; // Read the workbook data @@ -101,7 +102,8 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { return path; } - void withReader(Consumer action) throws IOException { + void withReader(ConsumerWithException action) + throws IOException, InterruptedException { try (var pkg = OPCPackage.open(path, PackageAccess.READ)) { var reader = new XSSFReader(pkg); action.accept(reader); @@ -115,7 +117,7 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { private record NamedRange(String name, String formula) {} - private void readWorkbookData() throws IOException { + private void readWorkbookData() throws IOException, InterruptedException { withReader( reader -> { try { @@ -124,6 +126,8 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { read1904DateSetting(workbookDoc); readSheetInfo(workbookDoc); readNamedRanges(workbookDoc); + } catch (ClosedByInterruptException e) { + throw new InterruptedException(e.getMessage()); } catch (SAXException | IOException | InvalidFormatException @@ -171,7 +175,7 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { } } - private synchronized void ensureReadShared() { + private synchronized void ensureReadShared() throws InterruptedException { if (hasReadShared) { return; } @@ -207,6 +211,8 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { styles = new XSSFReaderFormats(stylesTable); hasReadShared = true; + } catch (ClosedByInterruptException e) { + throw new InterruptedException(e.getMessage()); } catch (InvalidFormatException | IOException e) { throw new RuntimeException(e); } @@ -258,12 +264,12 @@ public class XSSFReaderWorkbook implements ExcelWorkbook { return namedRange == null ? null : namedRange.formula; } - public SharedStrings getSharedStrings() { + public SharedStrings getSharedStrings() throws InterruptedException { ensureReadShared(); return sharedStrings; } - public XSSFReaderFormats getStyles() { + public XSSFReaderFormats getStyles() throws InterruptedException { ensureReadShared(); return styles; } diff --git a/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java b/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java index 6f6b289e899..f4587fc6cb1 100644 --- a/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java +++ b/std-bits/table/src/main/java/org/enso/table/read/ExcelReader.java @@ -4,7 +4,6 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.poi.ss.util.CellReference; @@ -24,6 +23,7 @@ import org.enso.table.excel.ExcelSheet; import org.enso.table.excel.ExcelWorkbook; import org.enso.table.excel.ReadOnlyExcelConnection; import org.enso.table.problems.ProblemAggregator; +import org.enso.table.util.FunctionWithException; import org.graalvm.polyglot.Context; /** A table reader for MS Excel files. */ @@ -36,7 +36,8 @@ public class ExcelReader { * @return a String[] containing the sheet names. * @throws IOException when the action fails */ - public static String[] readSheetNames(File file, ExcelFileFormat format) throws IOException { + public static String[] readSheetNames(File file, ExcelFileFormat format) + throws IOException, InterruptedException { return withWorkbook(file, format, ExcelReader::readSheetNames); } @@ -65,7 +66,8 @@ public class ExcelReader { * @return a String[] containing the range names. * @throws IOException when the action fails */ - public static String[] readRangeNames(File file, ExcelFileFormat format) throws IOException { + public static String[] readRangeNames(File file, ExcelFileFormat format) + throws IOException, InterruptedException { return withWorkbook(file, format, ExcelWorkbook::getRangeNames); } @@ -89,7 +91,7 @@ public class ExcelReader { Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -130,7 +132,7 @@ public class ExcelReader { Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -175,7 +177,7 @@ public class ExcelReader { Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -202,7 +204,7 @@ public class ExcelReader { int skip_rows, Integer row_limit, ProblemAggregator problemAggregator) - throws InvalidLocationException { + throws InvalidLocationException, InterruptedException { int sheetIndex = workbook.getSheetIndex(rangeNameOrAddress); if (sheetIndex != -1) { return readTable( @@ -247,7 +249,7 @@ public class ExcelReader { Integer row_limit, ExcelFileFormat format, ProblemAggregator problemAggregator) - throws IOException, InvalidLocationException { + throws IOException, InvalidLocationException, InterruptedException { return withWorkbook( file, format, @@ -256,7 +258,10 @@ public class ExcelReader { } private static T withWorkbook( - File file, ExcelFileFormat format, Function action) throws IOException { + File file, + ExcelFileFormat format, + FunctionWithException action) + throws IOException, InterruptedException { try (ReadOnlyExcelConnection connection = ExcelConnectionPool.INSTANCE.openReadOnlyConnection(file, format)) { return connection.withWorkbook(action); @@ -270,7 +275,7 @@ public class ExcelReader { int skip_rows, Integer row_limit, ProblemAggregator problemAggregator) - throws InvalidLocationException { + throws InvalidLocationException, InterruptedException { int sheetIndex = workbook.getSheetIndex(excelRange.getSheetName()); if (sheetIndex == -1) { throw new InvalidLocationException( @@ -294,7 +299,8 @@ public class ExcelReader { ExcelHeaders.HeaderBehavior headers, int skipRows, int rowCount, - ProblemAggregator problemAggregator) { + ProblemAggregator problemAggregator) + throws InterruptedException { ExcelSheet sheet = workbook.getSheetAt(sheetIndex); diff --git a/std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java b/std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java new file mode 100644 index 00000000000..9f9343854bb --- /dev/null +++ b/std-bits/table/src/main/java/org/enso/table/util/ConsumerWithException.java @@ -0,0 +1,45 @@ +package org.enso.table.util; + +import java.util.Objects; + +/** + * Same as {@link java.util.function.Consumer} except that a one can declare a checked exception, E. + * Represents an operation that accepts a single input argument and returns no result. Unlike most + * other functional interfaces, {@code Consumer} is expected to operate via side-effects. + * + *

This is a functional interface whose functional method is + * {@link #accept(Object)}. + * + * @param the type of the input to the operation + * @param the type of the checked exception + */ +@FunctionalInterface +public interface ConsumerWithException { + + /** + * Performs this operation on the given argument. + * + * @param t the input argument + */ + void accept(T t) throws E; + + /** + * Returns a composed {@code Consumer} that performs, in sequence, this operation followed by the + * {@code after} operation. If performing either operation throws an exception, it is relayed to + * the caller of the composed operation. If performing this operation throws an exception, the + * {@code after} operation will not be performed. + * + * @param after the operation to perform after this operation + * @return a composed {@code Consumer} that performs in sequence this operation followed by the + * {@code after} operation + * @throws NullPointerException if {@code after} is null + */ + default ConsumerWithException andThen(java.util.function.Consumer after) + throws E { + Objects.requireNonNull(after); + return (T t) -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java b/std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java new file mode 100644 index 00000000000..5d41f4686cb --- /dev/null +++ b/std-bits/table/src/main/java/org/enso/table/util/FunctionWithException.java @@ -0,0 +1,51 @@ +package org.enso.table.util; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Same as {@link Function} except that a one can declare a checked exception, E. Represents a + * function that accepts one argument and produces a result. + * + *

This is a functional interface whose functional method is + * {@link #apply(Object)}. + * + * @param the type of the input to the function + * @param the type of the result of the function + * @param the type of the checked exception + */ +@FunctionalInterface +public interface FunctionWithException { + + /** + * Applies this function to the given argument. + * + * @param t the function argument + * @return the function result + */ + R apply(T t) throws E; + + default FunctionWithException compose( + FunctionWithException before) { + Objects.requireNonNull(before); + return (V v) -> apply(before.apply(v)); + } + + /** + * Returns a composed function that first applies this function to its input, and then applies the + * {@code after} function to the result. If evaluation of either function throws an exception, it + * is relayed to the caller of the composed function. + * + * @param the type of output of the {@code after} function, and of the composed function + * @param after the function to apply after this function is applied + * @return a composed function that first applies this function and then applies the {@code after} + * function + * @throws NullPointerException if after is null + * @see #compose(Function) + */ + default FunctionWithException andThen( + FunctionWithException after) { + Objects.requireNonNull(after); + return (T t) -> after.apply(apply(t)); + } +} diff --git a/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java b/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java index 2ced1f7a65b..48dbc780c44 100644 --- a/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java +++ b/std-bits/table/src/main/java/org/enso/table/write/ExcelWriter.java @@ -53,7 +53,8 @@ public class ExcelWriter { ExistingDataException, IllegalStateException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { if (sheetIndex == 0 || sheetIndex > workbook.getNumberOfSheets()) { int i = 1; while (workbook.getSheet("Sheet" + i) != null) { @@ -116,7 +117,8 @@ public class ExcelWriter { ExistingDataException, IllegalStateException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { int sheetIndex = workbook.getNumberOfSheets() == 0 ? -1 : workbook.getSheetIndex(sheetName); if (sheetIndex == -1) { writeTableToSheet( @@ -169,7 +171,8 @@ public class ExcelWriter { RangeExceededException, ExistingDataException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { Name name = workbook.getName(rangeNameOrAddress); ExcelRange excelRange; try { @@ -194,7 +197,8 @@ public class ExcelWriter { RangeExceededException, ExistingDataException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { int sheetIndex = workbook.getSheetIndex(range.getSheetName()); if (sheetIndex == -1) { throw new InvalidLocationException( @@ -263,7 +267,8 @@ public class ExcelWriter { throws RangeExceededException, ExistingDataException, ColumnNameMismatchException, - ColumnCountMismatchException { + ColumnCountMismatchException, + InterruptedException { Table mappedTable = switch (existingDataMode) { case APPEND_BY_INDEX -> ColumnMapper.mapColumnsByPosition( @@ -333,7 +338,7 @@ public class ExcelWriter { Long rowLimit, ExcelHeaders.HeaderBehavior headers, ExcelSheet sheet) - throws RangeExceededException, ExistingDataException { + throws RangeExceededException, ExistingDataException, InterruptedException { boolean writeHeaders = headers == ExcelHeaders.HeaderBehavior.USE_FIRST_ROW_AS_HEADERS; int requiredRows = Math.min(table.rowCount(), rowLimit == null ? Integer.MAX_VALUE : rowLimit.intValue()) @@ -383,7 +388,8 @@ public class ExcelWriter { * @param sheet Sheet containing the range. * @return True if range is empty and clear is False, otherwise returns False. */ - private static boolean rangeIsNotEmpty(Workbook workbook, ExcelRange range, ExcelSheet sheet) { + private static boolean rangeIsNotEmpty(Workbook workbook, ExcelRange range, ExcelSheet sheet) + throws InterruptedException { ExcelRange fullRange = range.getAbsoluteRange(workbook); for (int row = fullRange.getTopRow(); row <= fullRange.getBottomRow(); row++) { ExcelRow excelRow = sheet.get(row); @@ -401,7 +407,8 @@ public class ExcelWriter { * @param range The range to clear. * @param sheet Sheet containing the range. */ - private static void clearRange(Workbook workbook, ExcelRange range, ExcelSheet sheet) { + private static void clearRange(Workbook workbook, ExcelRange range, ExcelSheet sheet) + throws InterruptedException { ExcelRange fullRange = range.getAbsoluteRange(workbook); for (int row = fullRange.getTopRow(); row <= fullRange.getBottomRow(); row++) { ExcelRow excelRow = sheet.get(row); @@ -547,7 +554,7 @@ public class ExcelWriter { * @return EXCEL_COLUMN_NAMES if the range has headers, otherwise USE_FIRST_ROW_AS_HEADERS. */ private static ExcelHeaders.HeaderBehavior shouldWriteHeaders( - ExcelSheet excelSheet, int topRow, int startCol, int endCol) { + ExcelSheet excelSheet, int topRow, int startCol, int endCol) throws InterruptedException { ExcelRow row = excelSheet.get(topRow); // If the first row is missing or empty, should write headers.