Skip to content

Double-checked locking bugs#6662

Merged
leventov merged 6 commits intoapache:masterfrom
kamaci:feature-lockfix
Dec 7, 2018
Merged

Double-checked locking bugs#6662
leventov merged 6 commits intoapache:masterfrom
kamaci:feature-lockfix

Conversation

@kamaci
Copy link
Copy Markdown
Member

@kamaci kamaci commented Nov 26, 2018

Using double-checked locking for the lazy initialization of any other type of primitive or mutable object risks a second thread using an uninitialized or partially initialized member while the first thread is still creating it, and crashing the program.

@kamaci kamaci force-pushed the feature-lockfix branch 3 times, most recently from 406caaf to e996a2c Compare November 26, 2018 11:59
Copy link
Copy Markdown
Member

@leventov leventov left a comment

Choose a reason for hiding this comment

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

Thanks for these findings! The bugs are real, but they should be fixed differently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The former code here is indeed buggy, however putting synchronized on getUnderlyingProvider() is not how this method should be fixed.

Please make the following:

  1. Declare config field final
  2. Add a comment noting that FileSessionCredentialsProvider is safely initialized because it contains a final field sessionCredentials. Also a comment should be added on that field noting that final modifier should not be removed from it, because it has concurrency implications in LazyFileSessionCredentialsProvider.
  3. In getUnderlyingProvider(), provider should be read into a local variable, local variable updated in the synchronized block along with the field, and the local is returned in the last line of the method. This is because currently the second read of provider in the last line of the method could return null even if the first read in the first line returned non-null and the synchronized block was not entered.

Copy link
Copy Markdown
Member Author

@kamaci kamaci Nov 27, 2018

Choose a reason for hiding this comment

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

@leventov do you mean that?

  private final AWSCredentialsConfig config;
  @Nullable
  private FileSessionCredentialsProvider provider;

  public LazyFileSessionCredentialsProvider(AWSCredentialsConfig config)
  {
    this.config = config;
  }

  //FileSessionCredentialsProvider is safely initialized because it contains a final field sessionCredentials.
  private synchronized FileSessionCredentialsProvider getUnderlyingProvider()
  {
    FileSessionCredentialsProvider syncedProvider = provider;
    if (syncedProvider == null) {
      synchronized (config) {
        syncedProvider = new FileSessionCredentialsProvider(config.getFileSessionCredentials());
         provider = syncedProvider;
      }
    }
    return syncedProvider;
  }

Copy link
Copy Markdown
Member

@leventov leventov Nov 27, 2018

Choose a reason for hiding this comment

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

  1. No synchronized on the method
  2. There should be another check inside the synchronized block (because currently it's not double checked locking :)
  3. I would put the comment before the provider = syncedProvider; line:
// The provider field doesn't need to be volatile because FileSessionCredentialsProvider
// is safely initialized because it contains a final field sessionCredentials.

Added "The provider field doesn't need to be volatile" because that's what specifically needs to be explained here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also for the comment to be accurate, this PR should be merged not earlier than this one: #6664

Copy link
Copy Markdown
Contributor

@gianm gianm Nov 28, 2018

Choose a reason for hiding this comment

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

This post helps make sense of what @leventov is saying: https://shipilev.net/blog/2014/safe-public-construction/. I admit I had to read it before understanding what was meant by the comment "FileSessionCredentialsProvider is safely initialized because it contains a final field sessionCredentials".

@leventov - am I understanding you right - are you suggesting that we should rely on the fact that an object with at least one final field will always be safely initialized, and skip the volatile? i.e. in the terms used in the post linked above, you are suggesting it's best to do "Unsafe Local DCL + Safe Singleton" in this case?

If so, I don't feel super comfortable about that. The reason is that it is easy to screw it up: removing a final field in a different class (the class being initialized) breaks the holder class's correctness. Comments help with this problem, but IMO, it makes life easier to make the provider field volatile, which makes the correctness obvious from just looking at the container class (in this case, LazyFileSessionCredentialsProvider). Limiting the scope of what a maintainer needs to consider to a single file is a good thing, since it makes future maintenance easier and less error-prone.

Separately from that, it is not clear to me that this behavior is in fact guaranteed. Please educate me if I am missing something (I haven't studied the relevant sections of the Java spec) but it seems like the "do a totally safe initialization if any final is written" behavior is just an implementation detail of a particular VM and not something guaranteed. If that is right, then to get guaranteed safe initialization of an entire class, all fields should be final (or volatile).

Long story short, in this (non-perf-critical) case I am advocating for "Safe DCL", meaning the code below. And in performance critical code, "Unsafe Local DCL + Safe Singleton" could make sense, although it's worth verifying. The linked post above is 4 years old and it just did one set of tests, but, in that test that pattern didn't run any faster than "Safe DCL" or "Safe Local DCL" on x86.

private final AWSCredentialsConfig config;

@Nullable
private volatile FileSessionCredentialsProvider provider;

public LazyFileSessionCredentialsProvider(AWSCredentialsConfig config)
{
  this.config = config;
}

private FileSessionCredentialsProvider getUnderlyingProvider()
{
  if (provider == null) {
    synchronized (config) {
      if (provider == null) {
        provider = new FileSessionCredentialsProvider(config.getFileSessionCredentials());
      }
    }
  }
  return provider;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also since volatile you don't need that "safely initialized" comment on the field write line, but you need to write a similar comment on the field declaration, saying that the field is declared volatile in order to ensure safe publication of the object without worrying about final modifiers on the fields of the created object.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Here is the implementation with reference to Effective Java, Second Edition Item 71: Use lazy initialization judiciously:

  private final AWSCredentialsConfig config;

  /* The field is declared volatile in order to ensure safe publication of the object
   without worrying about final modifiers on the fields of the created object */
  @Nullable
  private volatile FileSessionCredentialsProvider provider;

  public LazyFileSessionCredentialsProvider(AWSCredentialsConfig config)
  {
    this.config = config;
  }

  //FileSessionCredentialsProvider is safely initialized because it contains a final field sessionCredentials.
  private FileSessionCredentialsProvider getUnderlyingProvider() {
    FileSessionCredentialsProvider syncedProvider = provider;
    if (syncedProvider == null) {
      synchronized (config) {
        syncedProvider = provider;
        if (syncedProvider == null) {
          syncedProvider = new FileSessionCredentialsProvider(config.getFileSessionCredentials());
          provider = syncedProvider;
        }
      }
    }
    return syncedProvider;
  }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 except the final thing, the comment should be a javadoc comment:

/**
 * The field is declared volatile in order to ensure safe publication of the object
 * in {@link #getUnderlyingProvider()} without worrying about final modifiers
 * on the fields of the created object
 */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This line should also be removed:

//FileSessionCredentialsProvider is safely initialized because it contains a final field sessionCredentials.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To extend Gian's point from #6662 (comment), there is another reason why I think that double-checked locking without volatile should be prohibited in Druid entirely. When some code is added that bypasses the initialization path, and only checks that the field is initialized, a-la

if (lazilyInitializedField != null) {
  doSomethingWith(lazilyInitializedField);
}

There is a race and possibility of NPE. The second read of the field might result in null, despite the first read returned non-null and the check was passed. The correct code is:

Foo lazilyInitializedField = this.lazilyInitializedField;
if (lazilyInitializedField != null) {
  doSomethingWith(lazilyInitializedField);
}

But I think it's unrealistic to expect this level of attention to details and discipline from all developers and reviewers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This class also requires changes, similar to points 2. and 3. LazyFileSessionCredentialsProvider.

For p. 2, please add a comment that the returned compiled script object is an anonymous inner class, therefore it has an implicit final field (a reference to the object of the enclosing class), therefore it is safely initialized without volatile on compiledScript.

For p. 3, checkAndCompileScript() should be made to return the compiledScript object and that object must be used when checkAndCompileScript() is called, instead of re-reading compiledScript.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as in JavaScriptAggregatorFactory

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as in JavaScriptAggregatorFactory

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add @Nullable to the provider field

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add @Nullable to compiledScript

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add @Nullable to fn

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add @Nullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as above

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add @Nullable to predicateFactory.

@leventov leventov added the Bug label Nov 26, 2018
@leventov leventov changed the title Double-checked locking bug is fixed. Double-checked locking bugs Nov 26, 2018
@kamaci kamaci force-pushed the feature-lockfix branch 3 times, most recently from 2adbacf to a9746d8 Compare December 3, 2018 09:33
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you please add dependency to org.checkerframework:checker-qual to the project and annotate this field @MonotonicNonNull?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still @Nullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@EnsuresNonNull("provider")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@MonotonicNonNull

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do you mean: https://checkerframework.org/api/org/checkerframework/checker/nullness/qual/MonotonicNonNull.html This is not included at druid-processing dependencies?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just for this part, documentation says that:

Use of @MonotonicNonNull on a static field is a code smell: it may indicate poor design. You should consider whether it is possible to make the field a member field that is set in the constructor.

Nullness Annotations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

compiledScript is not static

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same problem applies to interface import. Here is a related explanation: https://stackoverflow.com/questions/29097160/why-are-nonnull-annotation-on-fqn-not-allowed On demand static import solves the problem as follows, otherwise it gives that error:

Static Member Qualifying Type may Not be Annotated

  @MonotonicNonNull
  @Nullable
  private volatile ScriptAggregator compiledScript;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It shouldnt have MonotonicNonNull AND Nullable. Just MonotonicNonNull

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please make it to return a value. Rename to getCompiledScript(). @EnsuresNonNull("compiledScript")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as in other classes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All the same comments as in the prev. classes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All the same comments as in the prev. classes

@leventov
Copy link
Copy Markdown
Member

leventov commented Dec 3, 2018

@kamaci
Copy link
Copy Markdown
Member Author

kamaci commented Dec 3, 2018

@clintropolis
Copy link
Copy Markdown
Member

@kamaci note that you must not force push PR branch: https://github.com/apache/incubator-druid/blob/master/CONTRIBUTING.md#if-your-pull-request-shows-conflicts-with-master

I feel like maybe this is a bit less important now since github has added history for force pushes https://blog.github.com/changelog/2018-11-15-force-push-timeline-event/, at least as long as squashing/fixing are not done, unless there are other reasons I'm missing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still @Nullable

/**
* The field is declared volatile in order to ensure safe publication of the object
* in {@link #getUnderlyingProvider()} without worrying about final modifiers
* on the fields of the created object
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I realised that unlikely that many readers of this code will understand this passage without the reference to the thread. Please add , see https://github.com/apache/incubator-druid/pull/6662#discussion_r237013157 in the end

public Aggregator factorize(final ColumnSelectorFactory columnFactory)
{
checkAndCompileScript();
getCompiledScript();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This method should be used instead of compiledScript a few lines below, that was the whole point. Same in other places where getCompiledScript() is called.

/**
* The field is declared volatile in order to ensure safe publication of the object
* in {@link #compileScript(String, String, String)} without worrying about final modifiers
* on the fields of the created object
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

, see https://github.com/apache/incubator-druid/pull/6662#discussion_r237013157

/**
* The field is declared volatile in order to ensure safe publication of the object
* in {@link #compile(String)} without worrying about final modifiers
* on the fields of the created object
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

, see https://github.com/apache/incubator-druid/pull/6662#discussion_r237013157

* script compilation.
*/
@EnsuresNonNull("fn")
private void checkAndCompileScript()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please convert to lazy getter and use it as in the previous class.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All the same comments as in the prev. classes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All the same comments as in the prev. classes

Preconditions.checkState(config.isEnabled(), "JavaScript is disabled");

Function syncedFn = fn;
if (syncedFn == null) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@leventov Seems that here is always null at new case. Do I miss anything?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand you message.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Seems that first check for syncedFn

if (syncedFn == null)

is unnecessary.

On the other hand, I've just sent another commit to address your comments.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@leventov is everything OK for my PR?

public Aggregator factorize(final ColumnSelectorFactory columnFactory)
{
checkAndCompileScript();
compiledScript = getCompiledScript();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It reassigns the field again, that is not needed. Either those lines should be

JavaScriptAggregator.ScriptAggregator compiledScript = getCompiledScript();

Or (IMO) it's simpler just to use getCompiledScript() without introducing a local variable, in methods where it's needed only once.

Same below

public Object compute(Map<String, Object> combinedAggregators)
{
checkAndCompileScript();
fn = getCompiledScript();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as above, either introduce a local variable or just use getCompiledScript() in the expression, but don't reassign the volatile field.

public String apply(@Nullable Object value)
{
checkAndCompileScript();
fn = getCompiledScript();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as above.

* on the fields of the created object
*/
@MonotonicNonNull
private volatile Function<Object, String> fn;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Mention https://github.com/apache/incubator-druid/pull/6662#discussion_r237013157

* The field is declared volatile in order to ensure safe publication of the object
* in {@link JavaScriptPredicateFactory(String, ExtractionFn)} without worrying about final modifiers
* on the fields of the created object
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Mention https://github.com/apache/incubator-druid/pull/6662#discussion_r237013157

public Filter toFilter()
{
checkAndCreatePredicateFactory();
predicateFactory = getPredicateFactory();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same as above

public BufferAggregator factorizeBuffered(final ColumnSelectorFactory columnSelectorFactory)
{
checkAndCompileScript();
compiledScript = getCompiledScript();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still reassign. Also other occurrences in this PR.

@leventov
Copy link
Copy Markdown
Member

leventov commented Dec 7, 2018

@kamaci please merge master into your PR branch to make CI happy

@leventov
Copy link
Copy Markdown
Member

leventov commented Dec 7, 2018

However, not needed, CI passed itself

@leventov leventov merged commit bbb283f into apache:master Dec 7, 2018
@kamaci
Copy link
Copy Markdown
Member Author

kamaci commented Dec 7, 2018

👍

gianm pushed a commit to implydata/druid-public that referenced this pull request Dec 18, 2018
* Double-checked locking bug is fixed.

* @nullable is removed since there is no need to use along with @MonotonicNonNull.

* Static import is removed.

* Lazy initialization is implemented.

* Local variables used instead of volatile ones.

* Local variables used instead of volatile ones.
@jon-wei jon-wei added this to the 0.14.0 milestone Feb 20, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants