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

Only maintain internal widget ID seed if the user has called setWidgetIdSeed() #423

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

gadenbuie
Copy link
Collaborator

I've been working on trying to resolve an issue reported by users of the learnr package where htmlwidgets can end up providing the same widget ID for multiple widgets. In the end, I tracked this down to a somewhat unusual combination of htmlwidets' internal management of the widget ID RNG seed and learnr's use of forked processes for code evaluation.

Reprex

Here's a reprex that shows the gist of what happens in learnr. Suppose we have a global seed (this part isn't necessary but makes the reprex reproducible) and we call a function that creates a widget and internally calls createWidgetId().

pkgload::load_all()
#> ℹ Loading htmlwidgets
get_internal_seed_hash <- function() {
  digest::digest(.globals$idSeed)
}

set.seed(42)
createWidgetId()
#> [1] "30e4409849e39179307f"
get_internal_seed_hash()
#> [1] "20cd3cdb755b00bd24c9e6549834b19f"

After creating a widget ID, createWidgetId() copies the global RNG state and starts using it internally.

If we then fork our current process, we see that htmlwidget's internal id state is the same as the parent process (20cd3cd...) and we get a new ID.

id1 <- parallel::mcparallel({
  list(
    seed_hash = get_internal_seed_hash(),
    id = createWidgetId()
  )
})
parallel::mccollect(id1)[[1]]
#> $seed_hash
#> [1] "20cd3cdb755b00bd24c9e6549834b19f"
#> 
#> $id
#> [1] "2e17466358a46d139971"

If we end up running the same forked process again, we see that the internal RNG state of the new fork is the same as the first fork and the current global state — and we get a repeated widget ID.

id2 <- parallel::mcparallel({
  list(
    seed_hash = get_internal_seed_hash(),
    id = createWidgetId()
  )
})
parallel::mccollect(id2)[[1]]
#> $seed_hash
#> [1] "20cd3cdb755b00bd24c9e6549834b19f"
#> 
#> $id
#> [1] "2e17466358a46d139971"

The problem is that we can't mitigate this by calling set.seed() inside the forked process, because after the first call to createWidgetId() htmlwidgets uses and maintains its internal RNG state, forked from the global seed when the function was called.

id3 <- parallel::mcparallel({
  set.seed(123456)
  list(
    seed_hash = get_internal_seed_hash(),
    id = createWidgetId()
  )
})
parallel::mccollect(id3)[[1]]
#> $seed_hash
#> [1] "20cd3cdb755b00bd24c9e6549834b19f"
#> 
#> $id
#> [1] "2e17466358a46d139971"

Solution

The solution I'm proposing is for createWidgetId() to only manage an internal RNG state when the user has already called setWidgetIdSeed(). Otherwise, each call to createWidgetId() would use a new RNG state by temporarily removing the global seed.

set.seed(42)
createWidgetId()
#> [1] "77bce009e171413a52da"

id1 <- parallel::mcparallel({
  createWidgetId()
})
parallel::mccollect(id1)[[1]]
#> [1] "6b194a9bccb5f04a3941"

id2 <- parallel::mcparallel({
  createWidgetId()
})
parallel::mccollect(id2)[[1]]
#> [1] "7bc0d2cc33376a10d59a"

The upside is that createWidgetId() will always give a different ID unless the user has called setWidgetIdSeed(), thereby opting into a (possibly) reproducible widget ID series. OTOH, the downside is that some users may currently be depending on the fact that htmlwidgets was initially forking the global seed on first use. As you can see in the first call to createWidget(), the widget ID is always disconnected from the global seed — unless the widget ID seed is set explicitly.

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.

interactive maps with tmap & mapview display earlier chunks on shinyapps
1 participant