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

Race condition in scala.collection.concurrent.TrieMap.values.headOption #10177

Closed
scabug opened this issue Feb 9, 2017 · 5 comments
Closed

Comments

@scabug
Copy link

scabug commented Feb 9, 2017

I have the following code (where ownerRef is of type AtomicReference[Either[TrieMap[Thread, NestingLevel], (Thread, NestingLevel)]]):

			val maybeBlockingTxn = ownerRef.get.fold(_.values.headOption,{pair => Some(pair._2)})

I expect that when the Left branch of the fold is taken, the return value will be of type Option[NestingLevel]. Instead, with high-frequency in the application I am running, I get an exception whose stack trace bottoms out as follows:

[error] (run-main-0) java.util.NoSuchElementException: next on empty iterator
java.util.NoSuchElementException: next on empty iterator
	at scala.collection.Iterator$$anon$2.next(Iterator.scala:39)
	at scala.collection.Iterator$$anon$2.next(Iterator.scala:37)
	at scala.collection.concurrent.TrieMapIterator.next(TrieMap.scala:982)
	at scala.collection.concurrent.TrieMapIterator.next(TrieMap.scala:961)
	at scala.collection.MapLike$$anon$2.next(MapLike.scala:216)
	at scala.collection.IterableLike$class.head(IterableLike.scala:107)
	at scala.collection.AbstractIterable.head(Iterable.scala:54)
	at scala.collection.TraversableLike$class.headOption(TraversableLike.scala:412)
	at scala.collection.AbstractTraversable.headOption(Traversable.scala:104)

The exception originates in the line of code reproduced above.

@scabug
Copy link
Author

scabug commented Feb 9, 2017

Imported From: https://issues.scala-lang.org/browse/SI-10177?orig=1
Reporter: Thomas Dickerson (thomas.dickerson)
Affected Versions: 2.11.8

@scabug
Copy link
Author

scabug commented Feb 9, 2017

Thomas Dickerson (thomas.dickerson) said:
It appears that modifying _.values.headOption to _.readOnlySnapshot.values.headOption in my code fixes the problem, but it seems like highly unexpected behavior that the semantics of these differ (i.e. every iterator returned by functions on a TrieMap should be over a readOnlySnapshot, since this is the core feature advertised in the documention: {quote}It supports O(1), atomic, lock-free snapshots which are used to implement linearizable lock-free size, iterator and clear operations{quote})

@scabug
Copy link
Author

scabug commented Feb 9, 2017

@retronym said (edited on Feb 9, 2017 4:22:58 AM UTC):
Inlining some implementations of values, valuesIterator, headOption, head, and isEmpty:

  def fail[K, B](self: TrieMap[K, V]): Option[V] = {
    val v1: Iterable[V] = new AbstractIterable[V] with Iterable[V] with Serializable {
      def iterator = new AbstractIterator[V] {
        val iter = self.iterator
        def hasNext = iter.hasNext
        def next() = iter.next()._2
      }
      override def size = self.size
      override def foreach[U](f: V => U) = self.valuesIterator foreach f
    }
    if (v1.size == 0) None else Some(v1.iterator.next())
  }

Shows that the override of TrieMap#iterator as:

  def iterator: Iterator[(K, V)] =
    if (nonReadOnly) readOnlySnapshot().iterator
    else new TrieMapIterator(0, this)

Is insufficient to make .values.headOption work with a single version of the datastructure, as we end up calling TrieMap#iterator more than once within that operation.

An override like:

  override def valuesIterator: Iterator[V] = {
    if (nonReadOnly) readOnlySnapshot().valuesIterator
    else super.valuesIterator
  }

  override def keysIterator: Iterator[K] = {
    if (nonReadOnly) readOnlySnapshot().keysIterator
    else super.keysIterator
  }

WDYT, [~prokopec]?

@scabug
Copy link
Author

scabug commented Feb 9, 2017

@axel22 said (edited on Feb 9, 2017 4:14:26 PM UTC):
I though that if (v1.size == 0) None else Some(v1.iterator.next()) is where the race condition comes from, no? I mean, both size and iterator here result in calling the iterator separately.

Seems like you would have to create the snapshot in the new AbstractIterable ... only once, and use that for the iterator.

@scabug
Copy link
Author

scabug commented Feb 9, 2017

@retronym said:
@axel22 I've tried to restrict the change to TrieMap, rather than modifying MapLike.

scala/scala#5687

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

No branches or pull requests

2 participants