Skip to content

R6-encapsulated Shiny observer prevents the encapsulating object from being finalize()-ed #232

Open
@mmuurr

Description

@mmuurr

I'm not sure if this is an Issue (capital I), but I also can't find a good pattern here for when I embed a Shiny observer inside an R6 object. Take these two classes:

WithoutObs <- R6::R6Class("WithoutObs",
  private = list(
    finalize = function() {
      print(sprintf("%s: running finalize()", class(self)[1]))
    }
  ),
  public = list(
    initialize = function() {
      ## NOP
    }
  )
)

WithObs <- R6::R6Class("WithObs",
  private = list(
    obs = NULL,
    finalize = function() {
      print(sprintf("%s: running finalize()", class(self)[1]))
      private$obs$destroy()
    }
  ),
  public = list(
    initialize = function(rx) {
      private$obs <- shiny::observe(print(rx()))
    },
    destroy_obs = function() {
      private$obs$destroy()
    }
  )
)

Now when creating a few objects, then dropping our references to those objects, I'd expect the finalizers of both to run. Note that the finalizer for WithObs destroys the observer.

rxval <- shiny::reactiveVal(0)
without_obs <- WithoutObs$new()
with_obs <- WithObs$new(rxval)

rm(without_obs, with_obs)
gc()
# [1] "WithoutObs: running finalize()"
#          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
# Ncells 513748 27.5    1138677 60.9         NA   658348 35.2
# Vcells 941482  7.2    8388608 64.0      16384  1804906 13.8

with_obs's finalizer doesn't run. If we now flush Shiny and then garbage-collect now we get:

shiny:::flushReact()
# [1] 0  ## observer runs because the finalizer didn't run and thus didn't destroy()
gc()
#            used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
# Ncells  577265 30.9    1138677 60.9         NA   713492 38.2
# Vcells 1082047  8.3    8388608 64.0      16384  1804907 13.8

... still no finalizer.

In a brand new session, if we explicitly destroy the embedded observer, we can get the finalizer to run, but it requires a reactive flush, almost suggesting that the encapsulating object's finalizer is blocked until all other (external to the object) references to its fields have also been released?

rxval <- shiny::reactiveVal(0)
without_obs <- WithoutObs$new()
with_obs <- WithObs$new(rxval)
with_obs$destroy_obs()  ## destroy the observer ahead of time

rm(without_obs, with_obs)
gc()
# [1] "WithoutObs: running finalize()"
#          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
# Ncells 513755 27.5    1138697 60.9         NA   658348 35.2
# Vcells 941486  7.2    8388608 64.0      16384  1804921 13.8

shiny:::flushReact()
gc()
# [1] "WithObs: running finalize()"
#          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
# Ncells 516129 27.6    1138697 60.9         NA   658348 35.2
# Vcells 944092  7.3    8388608 64.0      16384  1804921 13.8

I'm surprised by both (i) the non-execution of with_obs's finalizer on the first gc() above and (ii) the need to both explicitly destroy the encapsulated observer and flush the reactive system to finally get the encapsulating object to finalize().

FWIW, my use case that led me down this path is pretty much exactly what you see with the WithObs class: an encapsulated observer that I want to destroy() when the program loses its reference to the encapsulating object -- specifically to try to prevent memory leaks by not accumulating unused/unneeded observers during a program lifecycle.

EDIT: forgot to mention, this is with R6 v2.5.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions