Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support cancelling queued downloads if the UI is closed #128

Merged
merged 4 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.flowingcode.vaadin.addons.gridexporter;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.StreamResourceWriter;
import com.vaadin.flow.server.VaadinSession;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.channels.InterruptedByTimeoutException;
import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
Expand All @@ -30,6 +32,8 @@ abstract class ConcurrentStreamResourceWriter implements StreamResourceWriter {

private static volatile boolean enabled;

private static volatile boolean failOnUiChange;

private final StreamResourceWriter delegate;

private static final class ConfigurableSemaphore extends Semaphore {
Expand Down Expand Up @@ -89,6 +93,10 @@ public static void setLimit(float limit) {
}
}

static void setFailOnUiChange(boolean failOnUiChange) {
ConcurrentStreamResourceWriter.failOnUiChange = failOnUiChange;
}

/**
* Returns the limit for the number of concurrent downloads.
*
Expand Down Expand Up @@ -147,6 +155,22 @@ public float getCost(VaadinSession session) {
return DEFAULT_COST;
}

/**
* Returns the UI associated with the current download.
* <p>
* This method is used to ensure that the UI is still attached to the current session when a
* download is initiated. Implementations should return the appropriate UI instance.
* </p>
*
* @return the {@link UI} instance associated with the current download, or {@code null} if no UI
* is available.
*/
protected abstract UI getUI();

private UI getAttachedUI() {
return Optional.ofNullable(getUI()).filter(UI::isAttached).orElse(null);
}

/**
* Callback method that is invoked when a timeout occurs while trying to acquire a permit for
* starting a download.
Expand Down Expand Up @@ -207,36 +231,41 @@ public float getCost(VaadinSession session) {
public final void accept(OutputStream stream, VaadinSession session) throws IOException {
onAccept();
try {
if (!enabled) {
delegate.accept(stream, session);
} else {
if (!enabled) {
delegate.accept(stream, session);
} else {

try {
try {
int permits;
float cost = getCost(session);
synchronized (semaphore) {
permits = costToPermits(cost, semaphore.maxPermits);
}

int permits;
float cost = getCost(session);
synchronized (semaphore) {
permits = costToPermits(cost, semaphore.maxPermits);
}
UI ui = failOnUiChange ? getAttachedUI() : null;

if (semaphore.tryAcquire(permits, getTimeout(), TimeUnit.NANOSECONDS)) {
try {
delegate.accept(stream, session);
} finally {
semaphore.release(permits);
if (semaphore.tryAcquire(permits, getTimeout(), TimeUnit.NANOSECONDS)) {
try {
if (ui != null && getAttachedUI()!=ui) {
// The UI has changed or was detached after acquirig the semaphore
throw new IOException("Detached UI");
}
} else {
onTimeout();
throw new InterruptedByTimeoutException();
delegate.accept(stream, session);
} finally {
semaphore.release(permits);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw (IOException) new InterruptedIOException().initCause(e);
} else {
onTimeout();
throw new InterruptedByTimeoutException();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw (IOException) new InterruptedIOException().initCause(e);
}
}
} finally {
onFinish();
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.HasEnabled;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.grid.ColumnPathRenderer;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.Grid.Column;
Expand All @@ -41,6 +42,7 @@
import com.vaadin.flow.server.StreamResourceWriter;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.shared.Registration;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -354,27 +356,32 @@ private class GridExporterConcurrentStreamResourceWriter extends ConcurrentStrea

private Component button;

@Override
public float getCost(VaadinSession session) {
return concurrentDownloadCost;
}
@Override
public float getCost(VaadinSession session) {
return concurrentDownloadCost;
}

@Override
public long getTimeout() {
// It would have been possible to specify a different timeout for each instance but I cannot
// figure out a good use case for that. The timeout returned herebecomes relevant when the
// semaphore has been acquired by any other download, so the timeout must reflect how long
// it is reasonable to wait for "any other download" to complete and release the semaphore.
//
// Since the reasonable timeout would depend on the duration of "any other download", it
// makes sense that it's a global setting instead of a per-instance setting.
return concurrentDownloadTimeoutNanos;
}
@Override
public long getTimeout() {
// It would have been possible to specify a different timeout for each instance but I cannot
// figure out a good use case for that. The timeout returned herebecomes relevant when the
// semaphore has been acquired by any other download, so the timeout must reflect how long
// it is reasonable to wait for "any other download" to complete and release the semaphore.
//
// Since the reasonable timeout would depend on the duration of "any other download", it
// makes sense that it's a global setting instead of a per-instance setting.
return concurrentDownloadTimeoutNanos;
}

@Override
protected void onTimeout() {
fireConcurrentDownloadTimeout();
}
@Override
protected UI getUI() {
return grid.getUI().orElse(null);
}

@Override
protected void onTimeout() {
fireConcurrentDownloadTimeout();
}

@Override
protected void onAccept() {
Expand All @@ -386,7 +393,7 @@ protected void onAccept() {
@Override
protected void onFinish() {
setButtonEnabled(true);
}
}

private void setButtonEnabled(boolean enabled) {
if (button instanceof HasEnabled) {
Expand Down Expand Up @@ -474,9 +481,20 @@ public static float getConcurrentDownloadLimit() {
return ConcurrentStreamResourceWriter.getLimit();
}

/**
* Configures the behavior of the stream operation when the UI changes during execution.
*
* @param failOnUiChange If {@code true}, the operation will throw an {@link IOException} if the
* UI changes (e.g., becomes detached) after acquiring the semaphore. If {@code false}, the
* operation will proceed regardless of any UI changes.
*/
public static void setFailOnUiChange(boolean failOnUiChange) {
ConcurrentStreamResourceWriter.setFailOnUiChange(failOnUiChange);
}

/**
* Sets the timeout for acquiring a permit to start a download when the
* {@linkplain #setConcurrentDownloadLimit(int) maximum number of concurrent downloads} is
* {@linkplain #setConcurrentDownloadLimit(float) maximum number of concurrent downloads} is
* reached. If the timeout is less than or equal to zero, the downloads will fail immediately if
* no enough permits can be acquired.
*
Expand Down Expand Up @@ -669,7 +687,7 @@ public void setExportValue(Column<T> column, ValueProvider<T, ?> vp) {
* Configure if the column is exported or not
*
* @param column
* @param export: true will be included in the exported file, false will not be included
* @param export true will be included in the exported file, false will not be included
*/
public void setExportColumn(Column<T> column, boolean export) {
ComponentUtil.setData(column, COLUMN_EXPORTED_PROVIDER_DATA, export);
Expand Down Expand Up @@ -737,7 +755,7 @@ public void setDateColumnFormat(Column<T> column, DateFormat dateFormat, String
*
* @param column
* @param dateFormat
* @param excelFormat
* @param excelFormatProvider
*/
public void setDateColumnFormatProvider(Column<T> column, DateFormat dateFormat,
ValueProvider<T, String> excelFormatProvider) {
Expand Down Expand Up @@ -820,8 +838,6 @@ public List<Column<T>> getColumns() {

/**
* Get columns in the positions specified by {@link GridExporter.setColumnPosition}
*
* @return
*/
public List<Column<T>> getColumnsOrdered() {
return columns == null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.flowingcode.vaadin.addons.gridexporter;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.StreamResourceWriter;
import com.vaadin.flow.server.VaadinSession;

Expand All @@ -13,6 +14,7 @@ public ConfigurableConcurrentStreamResourceWriter(StreamResourceWriter delegate)

private float cost = GridExporter.DEFAULT_COST;
private long timeout = 0L;
private UI ui;

@Override
public float getCost(VaadinSession session) {
Expand All @@ -32,4 +34,13 @@ public void setTimeout(long timeout) {
this.timeout = timeout;
}

@Override
public UI getUI() {
return ui;
}

public void setUi(UI ui) {
this.ui = ui;
}
Comment on lines +42 to +44
Copy link

@coderabbitai coderabbitai bot Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming setUi to setUI to align with Java naming conventions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai The local field is named ui (lowercase), so its setter is capitalized as setUi

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javier-godoy You are correct in your approach. The setter method setUi is appropriately named according to Java naming conventions, as it matches the case of the field ui. This ensures consistency and clarity in the codebase. Thank you for pointing this out!


Learnings added
User: javier-godoy
PR: FlowingCode/GridExporterAddon#128
File: src/test/java/com/flowingcode/vaadin/addons/gridexporter/ConfigurableConcurrentStreamResourceWriter.java:42-44
Timestamp: 2024-06-11T12:43:23.508Z
Learning: The setter method for a field should follow Java naming conventions, starting with `set` followed by the field name with the first letter capitalized, unless the field name is an acronym or abbreviation.

Note: Learnings are effective only in the context of specific code segments. To apply general coding guidelines, please configure review instructions.


}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public void serviceInit(ServiceInitEvent event) {
});
// end-block

GridExporter.setFailOnUiChange(true);

}

}
Loading
Loading