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

Intermittent runtime StackOverflowError with multi-level sealed hierarchy #216

Open
einholen opened this issue Mar 25, 2020 · 15 comments
Open

Comments

@einholen
Copy link

einholen commented Mar 25, 2020

Hi! I'm intermittently getting a StackOverflowError at runtime because the Magnolia macro resolves itself implicitly instead of the actual concrete type class.
I saw #66 and #190 but they seem to be different, so creating a new issue. Feel free to close if it's a dupe and unresolvable in Scala 2 🙂

So there's a sealed trait with a bunch of sealed subtraits each of which has a bunch of concrete case classes underneath. We use magnolia to dispatch specific type classes based on an instance of the sealed trait at runtime. We don't use combine, only dispatch. All branches of the hierarchy terminate, and each concrete class has an implicit defined that Magnolia is supposed to find.

With a hierarchy like this

sealed trait Base
sealed trait Sub1 extends Base { def param: Int }
case class Concrete1(param: Int) extends Sub1
sealed trait Sub2 extends Base { def param: String }
case class Concrete2(param: String) extends Sub2

and implicits for concrete case classes defined in a separate compilation unit from the Magnolia macro

object Show {
  type Typeclass[T] = Show[T]
  def combine[T <: Base](caseClass: CaseClass[Show, T]): Show[T] = throw new UnsupportedOperationException
  def dispatch[T <: Base](sealedTrait: SealedTrait[Show, T]): Show[T] = new Show[T] {
    def show(value: T): String = sealedTrait.dispatch(value) { subtype =>
      subtype.typeclass.show(subtype.cast(value))
    }
  }
  implicit def gen[T <: Base]: Show[T] = macro Magnolia.gen[T]
}

object Implicits {
  implicit val showConcrete1: Show[Concrete1] = (c: Concrete1) => s"Concrete1:${c.param}"
  implicit val showConcrete2: Show[Concrete2] = (c: Concrete2) => s"Concrete2:${c.param}"
}

Calling implicitly[Show[Base]].show(Concrete2("hello")) sometimes results in a runtime StackOverflowError.

After a clean compilation the error is gone, but changes to adjacent code seem to sometimes resurrect it.
What I'm seeing in debug inside sealedTrait.dispatch is, subtypesArray contains Concrete1 and Sub2. A value of Concrete2 thus matches Sub2, and then subtype.typeclass resolves to Show.gen again and again instead of recursing inside the sealed trait and finding the actual concrete implicit.
After a clean compilation, subtypesArray does not have any traits and properly has all case classes.

I could partially solve the problem by making the Show.gen non-implicit and calling it directly where needed - apparently without itself available as a "catch-all" implicit it understands that it needs to go deeper into the sealed hierarchy, and lands into dispatch twice: for Sub2 and then for Concrete2.

Scastie workbook with the full code: https://scastie.scala-lang.org/Hou8fzlAR0anAPeUHvWpFw
I couldn't reproduce the problem in Scastie - It works as intended, most likely because everything is in one file. We can emulate a runtime stackOverflow in this workbook, however, if we remove one of the concrete implicits (it won't be exactly the case I'm describing above but it would follow the same implicit resolution patterns as in my intermittent problem).

@einholen einholen changed the title Runtime StackOverflowError with nested sealed hierarchy Intermittent runtime StackOverflowError with nested sealed hierarchy Mar 25, 2020
@einholen einholen changed the title Intermittent runtime StackOverflowError with nested sealed hierarchy Intermittent runtime StackOverflowError with multi-level sealed hierarchy Mar 25, 2020
@propensive
Copy link
Collaborator

Thanks for the detailed writeup, @einholen! There is a certain amount global mutable state involved in running Magnolia, which is needed to persist some information between different invocations of the macro within the same compilation unit, but which unfortunately seems to persist between different invocations of the compiler. If there's more than one concurrent instance of the compiler handling compilations, then it can be even harder to diagnose.

But it does seem like we're still carrying over some state between compilations which we shouldn't be. I'm a little surprised that the outcome isn't a compiler crash. That would be better than a runtime error, frankly...

@einholen
Copy link
Author

einholen commented Mar 28, 2020

Ah I didn't know Magnolia (or macros in general) can carry over state between compilations. I think it makes sense. What I didn't mention because I thought it's irrelevant - we use Hydra in the project to speed up compilation by parallelizing it, so it probably contributes to the issue. I thought a clean compilation always fixes it but with this extra context from you I now think that it sometimes fixes it. We also don't use Hydra on CI and that could be why this problem never occurred in prod and I've only ever seen it when running unit tests locally.

@joroKr21
Copy link
Contributor

Oh yeah, expanding Magnolia macros in parallel is not well defined.
Perhaps it would be good to put a disclaimer in the README about Magnolia and Hydra,
but we need some way to check that this is in fact what is actually happening,
i.e. Magnlia macros being expanded in parallel.

@joroKr21
Copy link
Contributor

Another option would be to try and use ThreadLocal although I'm not sure if Hydra is using raw threads or Futures (ThreadLocal does not work with Future). @einholen do you have a clue?

@joroKr21
Copy link
Contributor

joroKr21 commented Mar 29, 2020

@propensive perhaps we should contact Triplequote to get a free licence for Magnolia and add it to our CI and try to reproduce the problem. WDYT?

@joroKr21
Copy link
Contributor

@einholen in the meantime you can try out #217 - you can build and publish Magnolia locally to do that.
Let me know if you need help with that.

@joroKr21
Copy link
Contributor

joroKr21 commented May 4, 2020

@einholen can you try the latest release?

@ghost
Copy link

ghost commented Jan 5, 2023

Posting a link to an IntelliJ specific issue with magnolia for posterity. The Scala compile server is backed by a thread pool which reuses long-running threads (whose thread local state never gets cleared), and having magnolia in the project seems to retain a large heap and could lead to OOM errors.

https://youtrack.jetbrains.com/issue/SCL-20909/Scala-compiler-server-memory-leak.#focus=Comments-27-6763912.0-0

I'm open to have a broader discussion about magnolia and the Scala compile server.

cc @joroKr21

@joroKr21
Copy link
Contributor

It might be as simple as clearing the local state at the end of macro expansion.

@ghost
Copy link

ghost commented Jan 10, 2023

I'm curious to try that out, from a practical standpoint, but I'm obviously not suggesting that any changes need to be done to the project, especially since I don't know whether that can be done safely and not have an adverse effect on the compilation, both in correctness and/or performance.

On the other hand, I also don't particularly feel like making some changes, or adding exceptions on the IntelliJ side, at least not before we have a bit more knowledge on the matter.

In any case, thanks for taking a look.

@adamw
Copy link
Member

adamw commented Jan 10, 2023

@joroKr21 it seems the thread locals are cleaned ...

def withContext(c: whitebox.Context)(fn: (Stack[c.type], Int) => c.Tree): c.Tree = {
val stack = threadLocalStack.get()
val workSet = threadLocalWorkSet.get()
workSet += c.macroApplication.symbol
val depth = c.enclosingMacros.count(m => workSet(m.macroApplication.symbol))
try fn(stack.asInstanceOf[Stack[c.type]], depth)
finally if (depth <= 1) {
stack.clear()
workSet.clear()
}
}

so it's probably sth more subtle than that :)

@joroKr21
Copy link
Contributor

Right I forgot about it. Maybe we also need to null out the thread locals themselves? But I doubt it.

neko-kai added a commit to 7mind/izumi that referenced this issue Jan 12, 2023
…ld magnolia. Intern pureconfig-magnolia into distage-config to update it.

* Details softwaremill/magnolia#216
neko-kai added a commit to 7mind/pureconfig that referenced this issue Jan 12, 2023
…void memory leak in old magnolia version

Details: softwaremill/magnolia#216 (comment)

Projects using pureconfig-magnolia cause IntelliJ compile server to run out of memory, but the leak seems to happen on old magnolia versions only.
neko-kai added a commit to 7mind/izumi that referenced this issue Jan 12, 2023
…ld magnolia. Intern pureconfig-magnolia into distage-config to update it.

* Details softwaremill/magnolia#216
neko-kai added a commit to 7mind/izumi that referenced this issue Jan 12, 2023
…ld magnolia. Intern pureconfig-magnolia into distage-config to update it. (#1858)

* Details softwaremill/magnolia#216
ruippeixotog pushed a commit to pureconfig/pureconfig that referenced this issue Jan 12, 2023
…void memory leak in old magnolia version (#1445)

Details: softwaremill/magnolia#216 (comment)

Projects using pureconfig-magnolia cause IntelliJ compile server to run out of memory, but the leak seems to happen on old magnolia versions only.
@Caparow
Copy link

Caparow commented Jan 13, 2023

@joroKr21 removing thread locals instead of just cleaning Stack and Set seems to have helped to resolve the issue. I've tested this with a project which uses Magnolia a lot.

May we release a new version with these changes?

  object Stack {
    // Cheating to satisfy Singleton bound (which improves type inference).
    private val dummyContext: whitebox.Context = null
    private val threadLocalStack = ThreadLocal.withInitial[Stack[dummyContext.type]](() => new Stack[dummyContext.type])
    private val threadLocalWorkSet = ThreadLocal.withInitial[mutable.Set[whitebox.Context#Symbol]](() => mutable.Set.empty)

    def withContext(c: whitebox.Context)(fn: (Stack[c.type], Int) => c.Tree): c.Tree = {
      val stack = threadLocalStack.get()
      val workSet = threadLocalWorkSet.get()
      workSet += c.macroApplication.symbol
      val depth = c.enclosingMacros.count(m => workSet(m.macroApplication.symbol))
      try fn(stack.asInstanceOf[Stack[c.type]], depth)
      finally if (depth <= 1) {
        threadLocalStack.remove()
        threadLocalWorkSet.remove()
      }
    }
  }

@adamw
Copy link
Member

adamw commented Jan 13, 2023

@Caparow can you maybe create a PR with this change? will be easiest to review

@Caparow
Copy link

Caparow commented Jan 13, 2023

@adamw I've created a PR #448

ruippeixotog pushed a commit to pureconfig/pureconfig that referenced this issue Jan 31, 2023
…void memory leak in old magnolia version (#1445)

Details: softwaremill/magnolia#216 (comment)

Projects using pureconfig-magnolia cause IntelliJ compile server to run out of memory, but the leak seems to happen on old magnolia versions only.
jcazevedo pushed a commit to pureconfig/pureconfig that referenced this issue Apr 19, 2023
* Enable coverage for Scala 3

* Update Scala 3 to v3.2.2-RC1 for testing

* Update enumeratum to 1.7.2 (#1426)

* Update sbt-pgp to 2.2.1 (#1427)

* Update joda-time to 2.12.2 (#1428)

* Update cats-effect to 3.4.2 (#1430)

* Update sbt-microsites to 1.4.0 (#1431)

* Update slf4j-simple to 2.0.6 (#1433)

* Update jekyll and html-proofer CI checks (#1444)

* Update cats-effect to 3.4.4 (#1437)

Co-authored-by: Rui Gonçalves <ruippeixotog@gmail.com>

* Update model:core to 1.5.4 (#1438)

* Update model:core to 1.5.4

* Update jekyll and html-proofer

* Revert jekyll to original version

* Revert jekyll to original version

* Update ruby and jekyll

* Fix htmlproofer arguments

Co-authored-by: Rui Gonçalves <ruippeixotog@gmail.com>

* Update sbt-microsites to 1.4.1 (#1439)

Co-authored-by: Rui Gonçalves <ruippeixotog@gmail.com>

* Update sbt to 1.8.2 (#1442)

Co-authored-by: Rui Gonçalves <ruippeixotog@gmail.com>

* Update scalatest to 3.2.15 (#1443)

Co-authored-by: Rui Gonçalves <ruippeixotog@gmail.com>

* Update http4s-core to 0.23.17 (#1441)

* pureconfig-magnolia: Update magnolia to softwaremill.magnolia1_2 to avoid memory leak in old magnolia version (#1445)

Details: softwaremill/magnolia#216 (comment)

Projects using pureconfig-magnolia cause IntelliJ compile server to run out of memory, but the leak seems to happen on old magnolia versions only.

* Add http4s022 compatibility module (#1421)

* add http4s022 module

* update docs

* fix README.md

* remove ip4s dependency from pureconfig-http4s module

Co-authored-by: Rui Gonçalves <ruippeixotog@gmail.com>

* Update http4s-core to 0.22.15 (#1448)

* Update magnolia to 1.1.3 (#1447)

* Update cats-effect to 3.4.5 (#1452)

* Update joda-convert to 2.2.3 (#1451)

* Update http4s-core to 0.23.18 (#1450)

* Update fs2-core, fs2-io to 3.5.0 (#1449)

* Update scalafmt-core to 3.7.0 (#1453)

* Update scalafmt-core to 3.7.1 (#1454)

* Update sbt-sonatype to 3.9.17 (#1455)

* Update mdoc, sbt-mdoc to 2.3.7 (#1456)

* Update model:core to 1.5.5 (#1457)

---------

Co-authored-by: Scala Steward <43047562+scala-steward@users.noreply.github.com>
Co-authored-by: Kai <450507+neko-kai@users.noreply.github.com>
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
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

No branches or pull requests

5 participants