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

Feature/marker pagination fixes #298

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

reftel
Copy link

@reftel reftel commented Oct 5, 2016

This set of patches solves an issue where pagination using a marker would miss entries due to a race condition. The fixes are mostly for the Hibernate adapter, but the problem likely exists for other adapters as well. Whether other adapters have the same issue has not been tested, but the MarkerRaceConditionTest should be able to reproduce the error given a suitable application-context.xml and test environment.

This makes it easier to debug issues that case 500 errors.
The implementation of AtomHopperJettyServerBuilder in test-suite did not
have a constructor that takes config file location as argument, as the
one in ah-jetty-server did. This change syncs the one in test-suite up
with the one in ah-jetty-server.
MarkerRaceConditionTest posts multiple entries in parallel, while
following the feed using the marker mechanism. The code is written to
try to maximize the risk of having multiple entries being handled in
parallel by the server. If any entry is written with an older timestamp
than the latest, then that entry will be missed, and this will be
revealed by the program.
When using a marker for pagination, entries were selected by comparing
their dateLastUpdated with the creation date of the marker, which causes
wrong results when using an updated marker.
This is required for high-resolution time stamps in Hibernate.
This, together with the hibernate-java8 module, enables use of
high-resolution timestamps.

In 5.2, the criteria support has been deprecated, and warnings are
logged on each use of them. Migrating to a different mechanism is a
large, separate task, so stay on 5.1 for now.
This reduces the probability of entries sharing the same timestamps.
Shared timestamps cause issues with pagination.
If application servers have clocks that are not in sync, then it is
possible for timestamps to not be increasing. If there are two
application servers with out-of-sync clocks, where the clock of server A
is a bit ahead of the clock of server B, then it's possible that an
entry written by A gets a higher timestamp than en entry written at a
later point by server B. If a consumer read the entry written by server
A, and then used it as a marker, then it would not receive the entry
written by server B.

This change makes all application servers ask the database server for
the clock, similar to how it is done in the Postgres adapter.
Otherwise, there's a race condition where newer entries can get older
timestamps: if server A starts a transaction, gets a timestamp, but then
gets blocked, and server B then starts a transaction, gets a higher
timstamp, and commits, then when server A resumes, its entry will be
written at a later time then the entry from server B, but with a lower
timestamp. A consumer that read the entry from server B and used it as a
marker would then not get the entry from server A. Locking the feed
before getting the timestamp and holding it until the commit solves this
issue, at the cost of serializing all writes to a feed.
Pagination doesn't work well when a marker has the same timestamp as
other entries. If one producer writes an entry, a consumer reads it, and
then another producer writes a new entry, and this gets the same
timestamp as the first entry, then it's not clear whether the consumer
should get this second entry or not when using the first entry as
marker. If entries with the same timestamp as the marker should be
included, then consumers may get entries multiple times if they share
timestamp with the marker. If entries with the same timestamp as the
marker should not be included, then consumers may miss entries.

Using secondary sorting on ID also doesn't work, since it then is
possible for a new entry that happen to get a lower-sorting ID to be
considered older than an existing entry with a higher-sorting ID.

This commit prevents these error conditions by forbidding sharing of
timestamps between entries in the same feed. That way, a producer that
happens to get the same timestamp as an earlier entry will have to
retry, giving it a new timestamp. This ensures that consumers get each
entry exactly once.

Due to the high resolution time stamps and the lock on the feed,
collisions should in practice never happen.
@cloudfeeds-jenkins-ci
Copy link

Can someone in the Cloud Feeds team review this patch?

@usnavi
Copy link
Contributor

usnavi commented Oct 5, 2016

ok to test.

@usnavi
Copy link
Contributor

usnavi commented Oct 5, 2016

thanks @reftel - we'll take a look at this.

Copy link
Contributor

@usnavi usnavi left a comment

Choose a reason for hiding this comment

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

Thanks again. Just a few comments and questions.

}

/*This should throw the error because */
@Test(expected=AtomDatabaseException.class)
@Test(expected=UnsupportedOperationException.class)
public void shouldThrowAtomDatabaseException() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we change the name of this test as its throwing a different exception?

@@ -39,11 +41,8 @@
@RunWith(Enclosed.class)
public class HibernateFeedRepositoryTest {

public static class WhenPerformingSimpleAction {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are you removing the WhenPerformingSimpleAction and the WhenPerformingComplexAction test classes?

Should WhenCreatingMisconfiguredFeedRepository be an additional class rather than replacing?

feedRepository.saveEntry(persistedEntry);

abderaParsedEntry.setUpdated(Date.from(persistedEntry.getDateLastUpdated()));
abderaParsedEntry.setPublished(Date.from(persistedEntry.getCreationDate()));

Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you flip the order on this?

import java.time.Instant;
import java.time.ZoneId;

public class NanoClock extends Clock
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this used anywhere?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants