Improve reproducability of module definitions (#1518)

This commit is contained in:
Jonatan Kłosko 2022-11-09 18:22:27 +01:00 committed by GitHub
parent 484e47142a
commit 936146e52c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 21 deletions

View file

@ -328,17 +328,25 @@ defmodule Livebook.Runtime.Evaluator do
set_pdict(context, state.ignored_pdict_keys) set_pdict(context, state.ignored_pdict_keys)
if old_context = state.contexts[ref] do
for module <- old_context.env.context_modules do
# If there is a deleted code for the module, we purge it first
:code.purge(module)
:code.delete(module)
end
end
start_time = System.monotonic_time() start_time = System.monotonic_time()
eval_result = eval(code, context.binding, context.env) eval_result = eval(code, context.binding, context.env)
evaluation_time_ms = time_diff_ms(start_time) evaluation_time_ms = time_diff_ms(start_time)
{result_context, result, code_error, identifiers_used, identifiers_defined} = {new_context, result, code_error, identifiers_used, identifiers_defined} =
case eval_result do case eval_result do
{:ok, value, binding, env} -> {:ok, value, binding, env} ->
tracer_info = Evaluator.IOProxy.get_tracer_info(state.io_proxy) tracer_info = Evaluator.IOProxy.get_tracer_info(state.io_proxy)
context_id = random_id() context_id = random_id()
result_context = %{ new_context = %{
id: context_id, id: context_id,
binding: binding, binding: binding,
env: prune_env(env, tracer_info), env: prune_env(env, tracer_info),
@ -346,21 +354,21 @@ defmodule Livebook.Runtime.Evaluator do
} }
{identifiers_used, identifiers_defined} = {identifiers_used, identifiers_defined} =
identifier_dependencies(result_context, tracer_info, context) identifier_dependencies(new_context, tracer_info, context)
result = {:ok, value} result = {:ok, value}
{result_context, result, nil, identifiers_used, identifiers_defined} {new_context, result, nil, identifiers_used, identifiers_defined}
{:error, kind, error, stacktrace, code_error} -> {:error, kind, error, stacktrace, code_error} ->
result = {:error, kind, error, stacktrace} result = {:error, kind, error, stacktrace}
identifiers_used = :unknown identifiers_used = :unknown
identifiers_defined = %{} identifiers_defined = %{}
# Empty context # Empty context
result_context = initial_context() new_context = initial_context()
{result_context, result, code_error, identifiers_used, identifiers_defined} {new_context, result, code_error, identifiers_used, identifiers_defined}
end end
state = put_context(state, ref, result_context) state = put_context(state, ref, new_context)
Evaluator.IOProxy.flush(state.io_proxy) Evaluator.IOProxy.flush(state.io_proxy)
Evaluator.IOProxy.clear_input_cache(state.io_proxy) Evaluator.IOProxy.clear_input_cache(state.io_proxy)
@ -386,7 +394,16 @@ defmodule Livebook.Runtime.Evaluator do
end end
defp handle_cast({:forget_evaluation, ref}, state) do defp handle_cast({:forget_evaluation, ref}, state) do
state = delete_context(state, ref) {context, state} = pop_context(state, ref)
for module <- context.env.context_modules do
# If there is a deleted code for the module, we purge it first
:code.purge(module)
:code.delete(module)
# And we immediately purge the newly deleted code
:code.purge(module)
end
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref}) Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
:erlang.garbage_collect(self()) :erlang.garbage_collect(self())
@ -446,14 +463,13 @@ defmodule Livebook.Runtime.Evaluator do
put_in(state.contexts[ref], context) put_in(state.contexts[ref], context)
end end
defp delete_context(state, ref) do defp pop_context(state, ref) do
update_evaluator_info(fn info -> update_evaluator_info(fn info ->
{_, info} = pop_in(info.contexts[ref]) {_, info} = pop_in(info.contexts[ref])
info info
end) end)
{_, state} = pop_in(state.contexts[ref]) pop_in(state.contexts[ref])
state
end end
defp update_evaluator_info(fun) do defp update_evaluator_info(fun) do
@ -494,6 +510,7 @@ defmodule Livebook.Runtime.Evaluator do
env env
|> Map.replace!(:aliases, Map.to_list(tracer_info.aliases_defined)) |> Map.replace!(:aliases, Map.to_list(tracer_info.aliases_defined))
|> Map.replace!(:requires, MapSet.to_list(tracer_info.requires_defined)) |> Map.replace!(:requires, MapSet.to_list(tracer_info.requires_defined))
|> Map.replace!(:context_modules, Map.keys(tracer_info.modules_defined))
end end
defp merge_context(prev_context, context) do defp merge_context(prev_context, context) do
@ -523,7 +540,7 @@ defmodule Livebook.Runtime.Evaluator do
end) end)
|> Map.update!(:aliases, &Keyword.merge(prev_env.aliases, &1)) |> Map.update!(:aliases, &Keyword.merge(prev_env.aliases, &1))
|> Map.update!(:requires, &:ordsets.union(prev_env.requires, &1)) |> Map.update!(:requires, &:ordsets.union(prev_env.requires, &1))
|> Map.replace!(:context_modules, []) |> Map.update!(:context_modules, &(&1 ++ prev_env.context_modules))
end end
@compile {:no_warn_undefined, {Code, :eval_quoted_with_env, 4}} @compile {:no_warn_undefined, {Code, :eval_quoted_with_env, 4}}
@ -590,7 +607,8 @@ defmodule Livebook.Runtime.Evaluator do
into: identifiers_used into: identifiers_used
identifiers_defined = identifiers_defined =
for {module, {version, _vars}} <- tracer_info.modules_defined, for {module, _vars} <- tracer_info.modules_defined,
version = module.__info__(:md5),
do: {{:module, module}, version}, do: {{:module, module}, version},
into: identifiers_defined into: identifiers_defined
@ -667,7 +685,7 @@ defmodule Livebook.Runtime.Evaluator do
# Note that :prune_binding removes variables used by modules # Note that :prune_binding removes variables used by modules
# (unless used outside), so we get those from the tracer # (unless used outside), so we get those from the tracer
module_used_vars = module_used_vars =
for {_module, {_version, vars}} <- tracer_info.modules_defined, for {_module, vars} <- tracer_info.modules_defined,
var <- vars, var <- vars,
into: MapSet.new(), into: MapSet.new(),
do: var do: var
@ -686,6 +704,8 @@ defmodule Livebook.Runtime.Evaluator do
do: var do: var
end end
defp prune_stacktrace([{Livebook.Runtime.Evaluator.Tracer, _fun, _arity, _meta} | _]), do: []
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372 # Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
# TODO: Remove else branch once we depend on the versions below # TODO: Remove else branch once we depend on the versions below
if System.otp_release() >= "25" do if System.otp_release() >= "25" do

View file

@ -21,7 +21,7 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
@doc false @doc false
def trace(event, env) do def trace(event, env) do
case to_updates(event, env) do case event_to_updates(event, env) do
[] -> [] ->
:ok :ok
@ -33,11 +33,21 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
:ok :ok
end end
defp to_updates(event, env) do defp event_to_updates(event, env) do
# Note that import/require/alias/defmodule don't trigger `:alias_reference` # Note that import/require/alias/defmodule don't trigger `:alias_reference`
# for the used alias, so we add it explicitly # for the used alias, so we add it explicitly
case event do case event do
:start ->
if Code.ensure_loaded?(env.module) do
raise CompileError,
line: env.line,
file: env.file,
description: "module #{inspect(env.module)} is already defined"
end
[]
{:import, _meta, module, _opts} -> {:import, _meta, module, _opts} ->
if(env.module, do: [], else: [:import_defined]) ++ if(env.module, do: [], else: [:import_defined]) ++
[{:module_used, module}, {:alias_used, module}] [{:module_used, module}, {:alias_used, module}]
@ -73,11 +83,10 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
{:remote_macro, _meta, module, _name, _arity} -> {:remote_macro, _meta, module, _name, _arity} ->
[{:module_used, module}, {:require_used, module}] [{:module_used, module}, {:require_used, module}]
{:on_module, bytecode, _ignore} -> {:on_module, _bytecode, _ignore} ->
module = env.module module = env.module
version = :erlang.md5(bytecode)
vars = Map.keys(env.versioned_vars) vars = Map.keys(env.versioned_vars)
[{:module_defined, module, version, vars}, {:alias_used, module}] [{:module_defined, module, vars}, {:alias_used, module}]
_ -> _ ->
[] []
@ -96,8 +105,8 @@ defmodule Livebook.Runtime.Evaluator.Tracer do
update_in(info.modules_used, &MapSet.put(&1, module)) update_in(info.modules_used, &MapSet.put(&1, module))
end end
defp apply_update(info, {:module_defined, module, version, vars}) do defp apply_update(info, {:module_defined, module, vars}) do
put_in(info.modules_defined[module], {version, vars}) put_in(info.modules_defined[module], vars)
end end
defp apply_update(info, {:alias_used, alias}) do defp apply_update(info, {:alias_used, alias}) do

View file

@ -277,6 +277,32 @@ defmodule Livebook.Runtime.EvaluatorTest do
ref = Process.monitor(widget_pid1) ref = Process.monitor(widget_pid1)
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason} assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
end end
test "raises when redefining a module in a different evaluation", %{evaluator: evaluator} do
code = """
defmodule Livebook.Runtime.EvaluatorTest.Redefinition do
end
"""
Evaluator.evaluate_code(evaluator, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
# Redefining in the same evaluation works
Evaluator.evaluate_code(evaluator, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
Evaluator.evaluate_code(evaluator, code, :code_2, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_2,
{:error, :error, %CompileError{}, []},
%{
code_error: %{
line: 1,
description:
"module Livebook.Runtime.EvaluatorTest.Redefinition is already defined"
}
}}
end
end end
describe "evaluate_code/6 identifier tracking" do describe "evaluate_code/6 identifier tracking" do
@ -584,6 +610,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
Process.put(:x, 1) Process.put(:x, 1)
Process.put(:y, 1) Process.put(:y, 1)
Process.put(:z, 1) Process.put(:z, 1)
defmodule Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingOne do
end
""" """
|> eval(evaluator, 0) |> eval(evaluator, 0)
@ -598,6 +627,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
Process.put(:y, 2) Process.put(:y, 2)
Process.delete(:z) Process.delete(:z)
defmodule Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingTwo do
end
""" """
|> eval(evaluator, 1) |> eval(evaluator, 1)
@ -616,6 +648,10 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1} assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1}
assert context.env.context_modules == [
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingOne
]
assert context.pdict == %{x: 1, y: 1, z: 1} assert context.pdict == %{x: 1, y: 1, z: 1}
# Evaluation 1 context # Evaluation 1 context
@ -635,6 +671,10 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert context.env.versioned_vars == %{{:y, nil} => 0} assert context.env.versioned_vars == %{{:y, nil} => 0}
assert context.env.context_modules == [
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingTwo
]
# Process dictionary is not diffed # Process dictionary is not diffed
assert context.pdict == %{x: 1, y: 2} assert context.pdict == %{x: 1, y: 2}
@ -654,6 +694,11 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1} assert context.env.versioned_vars == %{{:x, nil} => 0, {:y, nil} => 1}
assert context.env.context_modules == [
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingTwo,
Livebook.Runtime.EvaluatorTest.Identifiers.ContextMergingOne
]
assert context.pdict == %{x: 1, y: 2} assert context.pdict == %{x: 1, y: 2}
end end
end end
@ -686,6 +731,22 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason} assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
end end
test "deletes modules defined by the given evaluation", %{evaluator: evaluator} do
code = """
defmodule Livebook.Runtime.EvaluatorTest.ForgetEvaluation.Redefinition do
end
"""
Evaluator.evaluate_code(evaluator, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
Evaluator.forget_evaluation(evaluator, :code_1)
# Define the module in a different evaluation
Evaluator.evaluate_code(evaluator, code, :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:ok, _}, metadata()}
end
end end
describe "initialize_from/3" do describe "initialize_from/3" do