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

DateSerializer is not thread-safe #2429

Closed
cdegroc opened this issue Feb 3, 2021 · 1 comment · Fixed by #2430 or #2547
Closed

DateSerializer is not thread-safe #2429

cdegroc opened this issue Feb 3, 2021 · 1 comment · Fixed by #2430 or #2547

Comments

@cdegroc
Copy link
Contributor

cdegroc commented Feb 3, 2021

Setup

  • Version: 0.5.3
  • Storage Backend: cql
  • Mixed Index Backend: elasticsearch

Under production load, our JanusGraph cluster failed to process from 1 to 30 requests / hour due to ElasticSearch receiving invalid dates (+0000-01-31T08:49:54.000+0000):

org.janusgraph.core.JanusGraphException: Could not process individual retrieval call
[...]
    Caused by: org.janusgraph.diskstorage.PermanentBackendException: Permanent failure in storage backend
    at org.janusgraph.diskstorage.es.ElasticSearchIndex.query(ElasticSearchIndex.java:1117)
    at org.janusgraph.diskstorage.indexing.IndexTransaction.queryStream(IndexTransaction.java:108)
    at org.janusgraph.diskstorage.BackendTransaction$6.call(BackendTransaction.java:416)
    at org.janusgraph.diskstorage.BackendTransaction$6.call(BackendTransaction.java:413)
    at org.janusgraph.diskstorage.util.BackendOperation.executeDirect(BackendOperation.java:68)
    at org.janusgraph.diskstorage.util.BackendOperation.execute(BackendOperation.java:54)
    ... 49 common frames omitted
    Caused by: org.elasticsearch.client.ResponseException: method [POST], host [...], URI [/.../_search?scroll=60s], status line [HTTP/1.1 400 Bad Request]
{"error":{"root_cause":[{"type":"parse_exception","reason":"failed to parse date field [+0000-01-31T08:49:54.000+0000] with format [strict_date_optional_time||epoch_millis]: [failed to parse date field [+0000-01-31T08:49:54.000+0000] with format [strict_date_optional_time||epoch_millis]]"}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"...","node":"...","reason":{"type":"parse_exception","reason":"failed to parse date field [+0000-01-31T08:49:54.000+0000] with format [strict_date_optional_time||epoch_millis]: [failed to parse date field [+0000-01-31T08:49:54.000+0000] with format [strict_date_optional_time||epoch_millis]]","caused_by":{"type":"illegal_argument_exception","reason":"failed to parse date field [+0000-01-31T08:49:54.000+0000] with format [strict_date_optional_time||epoch_millis]","caused_by":{"type":"date_time_parse_exception","reason":"date_time_parse_exception: Failed to parse with all enclosed parsers"}}}}]},"status":400}
    at org.elasticsearch.client.RestClient.convertResponse(RestClient.java:283)
    at org.elasticsearch.client.RestClient.performRequest(RestClient.java:261)
    at org.elasticsearch.client.RestClient.performRequest(RestClient.java:235)
    at org.janusgraph.diskstorage.es.rest.RestElasticSearchClient.performRequest(RestElasticSearchClient.java:483)
    at org.janusgraph.diskstorage.es.rest.RestElasticSearchClient.search(RestElasticSearchClient.java:467)
    at org.janusgraph.diskstorage.es.rest.RestElasticSearchClient.search(RestElasticSearchClient.java:437)
    at org.janusgraph.diskstorage.es.rest.RestElasticSearchClient.search(RestElasticSearchClient.java:60)
    at org.janusgraph.diskstorage.es.ElasticSearchIndex.query(ElasticSearchIndex.java:1108)
    ... 54 common frames omitted

I tracked down the issue and was able to reproduce it with the following unit test:

public class DateSerializerTest {
    private final StandardSerializer serializer = new StandardSerializer();

    @Test
    public void dateSerializerConvertStringThreadSafe() throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(64);
        String input = "2021-01-30T17:30:31.000";
        Date reference = StdDateFormat.instance.parse(input);

        List<Future<Date>> futures = new ArrayList<>();
        try {
            // Have serializer parse the same date 100x in parallel
            for (int i = 0; i < 100; ++i) {
                futures.add(pool.submit(() -> serializer.convert(Date.class, input)));
            }
            for (Future<Date> future : futures) {
                assertEquals(reference, future.get());
            }
        } finally {
            pool.shutdown();
        }
    }
}

The unit test fails due to partial date matches such as:

org.opentest4j.AssertionFailedError: 
Expected :Sat Jan 30 18:30:31 CET 2021
Actual   :Fri Jan 01 18:30:31 CET 2021
org.opentest4j.AssertionFailedError: 
Expected :Sat Jan 30 18:30:31 CET 2021
Actual   :Thu Jan 01 01:00:00 CET 1970

I think DateSerializer is not thread-safe because StdDateFormat.instance#parse reuses a Calendar instance that is not thread-safe.

There are multiple ways to solve this (confirmed by the above unit test passing):

  • Wrap dateFormat.parse in a synchronized block/method
  • Store StdDateFormat in a ThreadLocal or create new local instances
  • Use Java 8+ DateTimeFormatter (if so, do we want to remain compatible with the RFC-1123 format that StdDateFormat handled?)
cdegroc added a commit to cdegroc/janusgraph that referenced this issue Feb 3, 2021
@cdegroc
Copy link
Contributor Author

cdegroc commented Feb 3, 2021

Created an initial PR. I'll be happy to get feedback and switch to a different approach.

cdegroc added a commit to cdegroc/janusgraph that referenced this issue Feb 5, 2021
@li-boxuan li-boxuan added this to the Release v0.6.0 milestone Feb 14, 2021
li-boxuan pushed a commit that referenced this issue Feb 14, 2021
Fixes #2429

Signed-off-by: Clement de Groc <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants