Repository navigation
Conversation
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
No unresolved issues remain, and focused regression coverage addresses stream behavior, retries, response alignment, and locale handling.
Review effort: Balanced
Findings: None
What changed in this PR
Reduces memory use and response parsing overhead in JanusGraph’s Elasticsearch bulk client, addressing #5024.
Changes:
- Sends serialized bulk items through a repeatable entity without assembling another body array.
- Filters responses to required fields and writes action metadata directly using locale-independent action names.
- Adds regression coverage and documents the optimization and result-accessor deprecation.
| File | Description |
|---|---|
| janusgraph-es/src/test/java/org/janusgraph/diskstorage/es/rest/RestClientRetryTest.java | Verifies retries send only failed items. |
| janusgraph-es/src/test/java/org/janusgraph/diskstorage/es/rest/RestClientBulkRequestsTest.java | Tests entity reads, mark/reset, filtering, and action serialization. |
| janusgraph-es/src/test/java/org/janusgraph/diskstorage/es/ElasticsearchIndexTest.java | Checks filtered failures retain correct document associations. |
| janusgraph-es/src/main/java/org/janusgraph/diskstorage/es/rest/RestElasticSearchClient.java | Implements copy-free bulk assembly, response filtering, and locale-safe actions. |
| janusgraph-es/src/main/java/org/janusgraph/diskstorage/es/rest/RestBulkResponse.java | Deprecates accessors for the omitted result field. |
| docs/changelog.md | Documents optimizations, locale correction, and deprecation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…responses A bulk request's body was written into a ByteArrayOutputStream, which grows by doubling, and then copied into an array, on every attempt, so a chunk held up to three more copies of its body at once: a bulk of 50,000 documents of 1 KB needed a heap of 320 MB or more. The body is now an entity which writes, and reads, the items' serialized bytes as they are, knows its length, can be sent again, and supports mark and reset for request signers; such a bulk completes in 144 MB. A bulk request asks Elasticsearch only for each item's status and error, under the actions JanusGraph sends, and for errors. Every item keeps its status, so the items still line up with the requests they answer. The action line of an item is written field by field instead of as a map through the ObjectMapper, and its action is lower-cased in the root locale: under a Turkish default locale INDEX became ındex, which Elasticsearch rejects. RestBulkItemResponse.getResult() is deprecated, as a filtered response carries no result. Fixes JanusGraph#5024 Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
porunov
force-pushed
the
es-bulk-request-path
branch
from
October 5, 2026 12:59
b6c84bd to
9fba248
Compare
porunov
added a commit
to porunov/janusgraph
that referenced
this pull request
Oct 5, 2026
toLowerCase() and toUpperCase() without a locale follow the JVM's default one, in which Turkish and Azerbaijani lower-case I to a dotless ı and upper-case i to a dotted İ. On such a JVM JanusGraph didn't know a mapping given in lower case, the type of a JTS geoshape, a time unit or a backend shorthand in upper case; allowed a key named ID; sent Elasticsearch geo queries with relations it rejects; named the Elasticsearch index of a store otherwise than other JVMs; and missed recovering Solr replicas. Names now convert in the root locale. Text which JanusGraph lower-cases for text predicates, in memory, for Elasticsearch queries and for Lucene, is lower-cased one code point at a time, as Lucene's LowerCaseFilter does in the analyzers of all three backends: String.toLowerCase differs from it for I in a Turkish locale and, in any locale, for the dotted capital I and a word-final sigma. Solr dates are formatted in the root locale. The one conversion left, the action of an Elasticsearch bulk request, is JanusGraph#5025's. Fixes JanusGraph#5026 Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
porunov
added a commit
to porunov/janusgraph
that referenced
this pull request
Oct 5, 2026
toLowerCase() and toUpperCase() without a locale follow the JVM's default one, in which Turkish and Azerbaijani lower-case I to a dotless ı and upper-case i to a dotted İ. On such a JVM JanusGraph didn't know a mapping given in lower case, the type of a JTS geoshape, a time unit or a backend shorthand in upper case; allowed a key named ID; sent Elasticsearch geo queries with relations it rejects; named the Elasticsearch index of a store otherwise than other JVMs; and missed recovering Solr replicas. Names now convert in the root locale. Text which JanusGraph lower-cases for text predicates, in memory, for Elasticsearch queries and for Lucene, is lower-cased one code point at a time, as Lucene's LowerCaseFilter does in the analyzers of all three backends: String.toLowerCase differs from it for I in a Turkish locale and, in any locale, for the dotted capital I and a word-final sigma. Solr dates are formatted in the root locale. The one conversion left, the action of an Elasticsearch bulk request, is JanusGraph#5025's. Fixes JanusGraph#5026 Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
porunov
added a commit
to porunov/janusgraph
that referenced
this pull request
Oct 5, 2026
toLowerCase() and toUpperCase() without a locale follow the JVM's default one, in which Turkish and Azerbaijani lower-case I to a dotless ı and upper-case i to a dotted İ. On such a JVM JanusGraph didn't know a mapping given in lower case, the type of a JTS geoshape, a time unit or a backend shorthand in upper case; allowed a key named ID; sent Elasticsearch geo queries with relations it rejects; named the Elasticsearch index of a store otherwise than other JVMs; and missed recovering Solr replicas. Names now convert in the root locale. Text which JanusGraph lower-cases for text predicates, in memory, for Elasticsearch queries and for Lucene, is lower-cased one code point at a time, as Lucene's LowerCaseFilter does in the analyzers of all three backends: String.toLowerCase differs from it for I in a Turkish locale and, in any locale, for the dotted capital I and a word-final sigma. Solr dates are formatted in the root locale. The one conversion left, the action of an Elasticsearch bulk request, is JanusGraph#5025's. Fixes JanusGraph#5026 Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
porunov
added a commit
to porunov/janusgraph
that referenced
this pull request
Oct 5, 2026
toLowerCase() and toUpperCase() without a locale follow the JVM's default one, in which Turkish and Azerbaijani lower-case I to a dotless ı and upper-case i to a dotted İ. On such a JVM JanusGraph didn't know a mapping given in lower case, the type of a JTS geoshape, a time unit or a backend shorthand in upper case; allowed a key named ID; sent Elasticsearch geo queries with relations it rejects; named the Elasticsearch index of a store otherwise than other JVMs; and missed recovering Solr replicas. Names now convert in the root locale. Text which JanusGraph lower-cases for text predicates, in memory, for Elasticsearch queries and for Lucene, is lower-cased one code point at a time, as Lucene's LowerCaseFilter does in the analyzers of all three backends: String.toLowerCase differs from it for I in a Turkish locale and, in any locale, for the dotted capital I and a word-final sigma. Solr dates are formatted in the root locale. The one conversion left, the action of an Elasticsearch bulk request, is JanusGraph#5025's. Fixes JanusGraph#5026 Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fixes #5024
An Elasticsearch bulk request held its body in memory up to four times to send it, parsed every field of a response of which the client reads two for each item, and built a map for the action line of each document. Its action also depended on the JVM's default locale.
Changes
BulkRequestEntity(new, inRestElasticSearchClient): the body of a bulk request, which writes the items' serialized bytes as they are (writeTo, which the gzip wrapper ofindex.[X].elasticsearch.compressionuses) and reads them through a stream (getContent, which the HTTP client uses otherwise). It knows its length and is repeatable, so the client can send it again to another node; a reattempt builds a new one over the failed items. Its stream behaves as theByteArrayInputStreamof the entity it replaces did: a read fills the buffer across items,available()counts what is left, and it supports mark and reset, which request signers such as those of the AWS SDK use to hash the body before it is sent. It replacesbuildBulkRequestInput, which copied the items into aByteArrayOutputStreamand from there into an array, on every attempt. Withindex.[X].elasticsearch.compressionthe client still compresses the body into a buffer of its own.filter_path=errors,items.index.status,items.index.error,items.update.status,items.update.error,items.delete.status,items.delete.error: each item's status and error, which the client reads, under the actions JanusGraph sends, anderrors, which keepsRestBulkResponse.isErrors()meaningful. Every item keeps its status, so the items line up with the requests they answer; filtering by the error alone leaves out the items which succeeded (checked against Elasticsearch 9.5.4). The actions are named fromElasticSearchMutation.RequestTyperather than matched with*, so that the query string holds no asterisk, which a request signer would have to percent-encode the way the cluster does. The response to 1,000 index requests is 25,026 bytes instead of 172,842.RestBulkItemResponse.getResult(), which nothing read, is deprecated, as the response no longer carries it.JsonGenerator, instead of as aHashMapin anImmutableMapserialized by theObjectMapper, and the action is lower-cased in the root locale. Under a Turkish default locale the action of an index request wasındex, with a dotless i, and Elasticsearch rejected the whole bulk request:Malformed action/metadata line [1], expected field [create], [delete], [index] or [update] but found [ındex].Measurements
A harness (not shipped) sends the same bulk of index requests through
RestElasticSearchClient.bulkRequestto Elasticsearch 9.5.4 in Docker (one shard, nothing indexed but the source), with master'sRestElasticSearchClientandRestBulkResponseagainst this branch's on the same classpath, in two rounds of alternating order. The time is the median per bulk; the allocation counts every thread, the HTTP client's I/O threads included.The smallest heap in which the bulk of 50,000 documents of 1 KB completes is 320 MB on master in one run and 384 MB in the other, below which it runs out of memory in
ByteArrayOutputStream. On this branch it is 144 MB, below which the serialization of the documents themselves runs out.Measured alone, building the item of a 100-byte document takes 229 ns instead of about 400, and of a 1 KB document about 790 ns instead of 1,440. Parsing a response takes 52 ns per item instead of 200.
Tests
RestClientBulkRequestsTest: the entity's body is the items' bytes in order, throughwriteToand through its stream read twice, and its length is theirs; the stream gives the same bytes whatever the size of the buffer and the offset it reads into, filling each buffer across items, withavailable()counting down, and reads the rest again after a reset from a mark at every position, or from the start without one; a bulk request carries the filter and the entity; the action line of each operation, with characters which JSON escapes, the mapping type for Elasticsearch 6, and a Turkish default locale, which yieldsındexwhen the action is lower-cased in the default locale.RestClientRetryTest: the reattempt of a bulk request sends the failed item alone.ElasticsearchIndexTest.testTheFailedItemsOfABulkResponseAreReportedForTheirOwnDocuments(new, Elasticsearch in a container): updates of missing documents between index requests which succeed fail with a 404, and the documents reported are exactly theirs. With a filter of the errors alone it fails, as the successful items are left out and the failures shift onto other documents.ElasticsearchIndexTest.testCompressedRequestssends bulk requests through the gzip wrapper, which writes the entity withwriteTo.ElasticsearchIndexTest(288) and theRestClient*Testclasses (77) pass on the final code.BerkeleyElasticsearchTest(92, 1 skipped),ElasticsearchConfigTest(12) andElasticSearchIndexReattemptTest(7) passed before the last two changes, mark and reset, and the actions named in the filter.🤖 Generated with Claude Code