-
Notifications
You must be signed in to change notification settings - Fork 241
SYM-2983: Resume transfer of large batches using HTTP Range headers #973
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
Draft
evan-miller-jumpmind
wants to merge
11
commits into
release/3.18
Choose a base branch
from
feature/2983_resume_transfer_of_large_batches
base: release/3.18
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
09da473
SYM-2983: Resume transfer of large batches using HTTP Range headers
evan-miller-jumpmind 45419ba
Merge branch 'release/3.18' into feature/2983_resume_transfer_of_larg…
evan-miller-jumpmind b76d0bf
SYM-2983: Addressed Sonar alerts
evan-miller-jumpmind f34924e
SYM-2983: Fixed another Sonar alert
evan-miller-jumpmind 75a9c41
SYM-2983: Addressed PR comments
evan-miller-jumpmind 7fbccf7
SYM-2983: Fixed Sonar alerts
evan-miller-jumpmind 30ec388
SYM-2983: Fixed Sonar warning
evan-miller-jumpmind d1838c1
SYM-2983: Fixed Sonar warning
evan-miller-jumpmind 1d3edbc
Merge branch 'release/3.18' into feature/2983_resume_transfer_of_larg…
pavel-jm 2ad2142
Merge branch 'release/3.18' into feature/2983_resume_transfer_of_larg…
evan-miller-jumpmind 1752f2f
SYM-2983: Resolved merge conflicts and updated license headers
evan-miller-jumpmind File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
symmetric-core/src/main/java/org/jumpmind/symmetric/extract/CountingSkippingWriter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /** | ||
| * Licensed to JumpMind Inc under one or more contributor | ||
| * license agreements. See the NOTICE file distributed | ||
| * with this work for additional information regarding | ||
| * copyright ownership. JumpMind Inc licenses this file | ||
| * to you under the GNU Affero General Public License, version 3.0 (AGPLv3) | ||
| * (the "License"); you may not use this file except in compliance | ||
| * with the License. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License, | ||
| * version 3.0 (AGPLv3) along with this library; if not, see | ||
| * <http://www.gnu.org/licenses/>. | ||
| * | ||
| * 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.jumpmind.symmetric.extract; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.Writer; | ||
|
|
||
| /** | ||
| * Wraps a destination {@link Writer}, discarding the first {@code skipCount} characters written to it and forwarding the rest, while counting the total number | ||
| * of characters seen (skipped plus forwarded). This lets a single deterministic write pass serve both a full batch resend ({@code skipCount == 0}) and a | ||
| * resumed, partial send ({@code skipCount > 0}) starting from the same point in the stream. | ||
| * <p> | ||
| * The count is in decoded characters of the underlying CSV text stream, not raw network bytes: the staged resource is read and written through | ||
| * {@link java.io.Reader}/{@link java.io.Writer}, not {@link java.io.InputStream}/{@link java.io.OutputStream}, so an HTTP Range/Content-Range value used with | ||
| * this class must agree on that same unit on both the client and server side. Since both sides read the exact same staged, UTF-8 file deterministically, this | ||
| * is internally consistent even though it is not a literal byte offset per RFC 9110 Range semantics. | ||
| */ | ||
| public class CountingSkippingWriter extends Writer { | ||
| private final Writer delegate; | ||
| private final long skipCount; | ||
| private long totalCount; | ||
|
|
||
| public CountingSkippingWriter(Writer delegate, long skipCount) { | ||
| this.delegate = delegate; | ||
| this.skipCount = skipCount; | ||
| } | ||
|
|
||
| public long getTotalCount() { | ||
| return totalCount; | ||
| } | ||
|
|
||
| @Override | ||
| public void write(char[] cbuf, int off, int len) throws IOException { | ||
| int writeOff = off; | ||
| int writeLen = len; | ||
| if (totalCount < skipCount) { | ||
| long remainingToSkip = skipCount - totalCount; | ||
| int skipInThisChunk = (int) Math.min(remainingToSkip, len); | ||
| writeOff = off + skipInThisChunk; | ||
| writeLen = len - skipInThisChunk; | ||
| } | ||
| if (writeLen > 0) { | ||
| delegate.write(cbuf, writeOff, writeLen); | ||
| } | ||
| totalCount += len; | ||
| } | ||
|
|
||
| @Override | ||
| public void flush() throws IOException { | ||
| delegate.flush(); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() throws IOException { | ||
| delegate.close(); | ||
| } | ||
| } | ||
92 changes: 92 additions & 0 deletions
92
symmetric-core/src/main/java/org/jumpmind/symmetric/file/FileSyncBatchEnvelope.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /** | ||
| * Licensed to JumpMind Inc under one or more contributor | ||
| * license agreements. See the NOTICE file distributed | ||
| * with this work for additional information regarding | ||
| * copyright ownership. JumpMind Inc licenses this file | ||
| * to you under the GNU Affero General Public License, version 3.0 (AGPLv3) | ||
| * (the "License"); you may not use this file except in compliance | ||
| * with the License. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License, | ||
| * version 3.0 (AGPLv3) along with this library; if not, see | ||
| * <http://www.gnu.org/licenses/>. | ||
| * | ||
| * 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.jumpmind.symmetric.file; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.OutputStream; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| import org.jumpmind.symmetric.io.stage.StagedResourceETag; | ||
|
|
||
| /** | ||
| * A lightweight header written immediately before each batch's complete, independently-staged zip bytes in a {@code FileSync-Format}-tagged pull response, so | ||
| * several batches can be bundled into one response while still letting the reader know exactly where one batch's zip ends and the next one's header begins — a | ||
| * purely length-based framing, no entry-by-entry inspection required. | ||
| * <p> | ||
| * Wire shape is one UTF-8 text line, {@code <batchId>,<zipByteLength>,<etagJson>}, followed by exactly {@code zipByteLength} raw zip bytes. The ETag JSON | ||
| * itself may contain commas, so only the first two commas are treated as delimiters; everything after the second comma is taken verbatim as the ETag JSON. | ||
| */ | ||
| public class FileSyncBatchEnvelope { | ||
| private final long batchId; | ||
| private final long length; | ||
| private final StagedResourceETag etag; | ||
|
|
||
| public FileSyncBatchEnvelope(long batchId, long length, StagedResourceETag etag) { | ||
| this.batchId = batchId; | ||
| this.length = length; | ||
| this.etag = etag; | ||
| } | ||
|
|
||
| public long getBatchId() { | ||
| return batchId; | ||
| } | ||
|
|
||
| public long getLength() { | ||
| return length; | ||
| } | ||
|
|
||
| public StagedResourceETag getEtag() { | ||
| return etag; | ||
| } | ||
|
|
||
| public static void writeHeader(OutputStream out, long batchId, long length, StagedResourceETag etag) throws IOException { | ||
| String line = batchId + "," + length + "," + etag.toJson() + "\n"; | ||
| out.write(line.getBytes(StandardCharsets.UTF_8)); | ||
| } | ||
|
|
||
| /** | ||
| * Reads one envelope header line from {@code in}, one byte at a time so as to never consume bytes past the header's trailing newline — the caller must read | ||
| * exactly {@link #getLength()} bytes immediately afterward, so any over-read here would corrupt the following batch's zip content. | ||
| * | ||
| * @return the parsed header, or {@code null} at a clean end of stream (no more batches follow) | ||
| */ | ||
| public static FileSyncBatchEnvelope readHeader(InputStream in) throws IOException { | ||
| StringBuilder line = new StringBuilder(); | ||
| int b; | ||
| while ((b = in.read()) != -1 && b != '\n') { | ||
| line.append((char) b); | ||
| } | ||
| if (b == -1 && line.isEmpty()) { | ||
| return null; | ||
| } | ||
| String headerLine = line.toString(); | ||
| int firstComma = headerLine.indexOf(','); | ||
| int secondComma = firstComma < 0 ? -1 : headerLine.indexOf(',', firstComma + 1); | ||
| if (firstComma < 0 || secondComma < 0) { | ||
| throw new IOException("Malformed file sync envelope header: " + headerLine); | ||
| } | ||
| long batchId = Long.parseLong(headerLine.substring(0, firstComma)); | ||
| long length = Long.parseLong(headerLine.substring(firstComma + 1, secondComma)); | ||
| StagedResourceETag etag = StagedResourceETag.fromJson(headerLine.substring(secondComma + 1)); | ||
| return new FileSyncBatchEnvelope(batchId, length, etag); | ||
| } | ||
| } |
150 changes: 150 additions & 0 deletions
150
symmetric-core/src/main/java/org/jumpmind/symmetric/file/FileSyncPullResult.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| /** | ||
| * Licensed to JumpMind Inc under one or more contributor | ||
| * license agreements. See the NOTICE file distributed | ||
| * with this work for additional information regarding | ||
| * copyright ownership. JumpMind Inc licenses this file | ||
| * to you under the GNU Affero General Public License, version 3.0 (AGPLv3) | ||
| * (the "License"); you may not use this file except in compliance | ||
| * with the License. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License, | ||
| * version 3.0 (AGPLv3) along with this library; if not, see | ||
| * <http://www.gnu.org/licenses/>. | ||
| * | ||
| * 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.jumpmind.symmetric.file; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.jumpmind.symmetric.io.stage.IStagedResource; | ||
| import org.jumpmind.symmetric.io.stage.StagedResourceETag; | ||
| import org.jumpmind.symmetric.model.OutgoingBatch; | ||
|
|
||
| /** | ||
| * The outcome of {@link org.jumpmind.symmetric.service.IFileSyncService#prepareFilesForPull}, carrying both what the servlet handler needs to set response | ||
| * headers/status, and what {@link org.jumpmind.symmetric.service.IFileSyncService#writeFilesForPull} needs to stream the previously-staged bytes afterward. | ||
| * Split from a single combined call so the handler can set headers on the servlet response <em>before</em> any bytes are written to it - setting a header on an | ||
| * already-committed response is a silent no-op, which previously meant the {@code FileSync-Format} header was never actually sent to the client. | ||
| * <p> | ||
| * {@code resumeEtag} is non-null only when this response served (or attempted to serve) exactly one specific, previously-interrupted batch by request; it is | ||
| * {@code null} for a normal, non-resume pull. {@code allRequestedBatches} is only meaningful for a normal (non-resume) pull - it is the full candidate list | ||
| * {@code batches} was selected from, needed by {@link org.jumpmind.symmetric.service.IFileSyncService#writeFilesForPull} to mark them loaded. | ||
| */ | ||
| public class FileSyncPullResult { | ||
| private final List<OutgoingBatch> batches; | ||
| private final List<OutgoingBatch> allRequestedBatches; | ||
| private final List<IStagedResource> stagedResources; | ||
| private final boolean isEnvelopeFormatUsed; | ||
| private final boolean isPartialContent; | ||
| private final StagedResourceETag resumeEtag; | ||
| private final long totalSize; | ||
| private final long skipCount; | ||
|
|
||
| private FileSyncPullResult(Builder builder) { | ||
| this.batches = builder.batches; | ||
| this.allRequestedBatches = builder.allRequestedBatches; | ||
| this.stagedResources = builder.stagedResources; | ||
| this.isEnvelopeFormatUsed = builder.isEnvelopeFormatUsed; | ||
| this.isPartialContent = builder.isPartialContent; | ||
| this.resumeEtag = builder.resumeEtag; | ||
| this.totalSize = builder.totalSize; | ||
| this.skipCount = builder.skipCount; | ||
| } | ||
|
|
||
| public static Builder builder() { | ||
| return new Builder(); | ||
| } | ||
|
|
||
| public List<OutgoingBatch> getBatches() { | ||
| return batches; | ||
| } | ||
|
|
||
| public List<OutgoingBatch> getAllRequestedBatches() { | ||
| return allRequestedBatches; | ||
| } | ||
|
|
||
| public List<IStagedResource> getStagedResources() { | ||
| return stagedResources; | ||
| } | ||
|
|
||
| public boolean isEnvelopeFormatUsed() { | ||
| return isEnvelopeFormatUsed; | ||
| } | ||
|
|
||
| public boolean isPartialContent() { | ||
| return isPartialContent; | ||
| } | ||
|
|
||
| public StagedResourceETag getResumeEtag() { | ||
| return resumeEtag; | ||
| } | ||
|
|
||
| public long getTotalSize() { | ||
| return totalSize; | ||
| } | ||
|
|
||
| public long getSkipCount() { | ||
| return skipCount; | ||
| } | ||
|
|
||
| public static class Builder { | ||
| private List<OutgoingBatch> batches; | ||
| private List<OutgoingBatch> allRequestedBatches; | ||
| private List<IStagedResource> stagedResources; | ||
| private boolean isEnvelopeFormatUsed; | ||
| private boolean isPartialContent; | ||
| private StagedResourceETag resumeEtag; | ||
| private long totalSize; | ||
| private long skipCount; | ||
|
|
||
| public Builder batches(List<OutgoingBatch> batches) { | ||
| this.batches = batches; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder allRequestedBatches(List<OutgoingBatch> allRequestedBatches) { | ||
| this.allRequestedBatches = allRequestedBatches; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder stagedResources(List<IStagedResource> stagedResources) { | ||
| this.stagedResources = stagedResources; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder envelopeFormatUsed(boolean isEnvelopeFormatUsed) { | ||
| this.isEnvelopeFormatUsed = isEnvelopeFormatUsed; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder partialContent(boolean isPartialContent) { | ||
| this.isPartialContent = isPartialContent; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder resumeEtag(StagedResourceETag resumeEtag) { | ||
| this.resumeEtag = resumeEtag; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder totalSize(long totalSize) { | ||
| this.totalSize = totalSize; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder skipCount(long skipCount) { | ||
| this.skipCount = skipCount; | ||
| return this; | ||
| } | ||
|
|
||
| public FileSyncPullResult build() { | ||
| return new FileSyncPullResult(this); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.