Cancel scheduled evaluation on error (#1688)

This commit is contained in:
Jonatan Kłosko 2023-02-03 21:04:13 +01:00 committed by GitHub
parent c29b6fdda2
commit 597d9fff3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 337 additions and 57 deletions

View file

@ -280,6 +280,7 @@ const Session = {
faviconForEvaluationStatus(evaluationStatus) { faviconForEvaluationStatus(evaluationStatus) {
if (evaluationStatus === "evaluating") return "favicon-evaluating"; if (evaluationStatus === "evaluating") return "favicon-evaluating";
if (evaluationStatus === "stale") return "favicon-stale"; if (evaluationStatus === "stale") return "favicon-stale";
if (evaluationStatus === "errored") return "favicon-errored";
return "favicon"; return "favicon";
}, },

View file

@ -68,11 +68,8 @@ defmodule Livebook.LiveMarkdown.Export do
end end
defp notebook_metadata(notebook) do defp notebook_metadata(notebook) do
put_unless_default( keys = [:persist_outputs, :autosave_interval_s]
%{}, put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
Map.take(notebook, [:persist_outputs, :autosave_interval_s]),
Map.take(Notebook.new(), [:persist_outputs, :autosave_interval_s])
)
end end
defp render_section(section, notebook, ctx) do defp render_section(section, notebook, ctx) do
@ -148,11 +145,8 @@ defmodule Livebook.LiveMarkdown.Export do
end end
defp cell_metadata(%Cell.Code{} = cell) do defp cell_metadata(%Cell.Code{} = cell) do
put_unless_default( keys = [:disable_formatting, :reevaluate_automatically, :continue_on_error]
%{}, put_unless_default(%{}, Map.take(cell, keys), Map.take(Cell.Code.new(), keys))
Map.take(cell, [:disable_formatting, :reevaluate_automatically]),
Map.take(Cell.Code.new(), [:disable_formatting, :reevaluate_automatically])
)
end end
defp cell_metadata(_cell), do: %{} defp cell_metadata(_cell), do: %{}

View file

@ -393,6 +393,9 @@ defmodule Livebook.LiveMarkdown.Import do
{"reevaluate_automatically", reevaluate_automatically}, attrs -> {"reevaluate_automatically", reevaluate_automatically}, attrs ->
Map.put(attrs, :reevaluate_automatically, reevaluate_automatically) Map.put(attrs, :reevaluate_automatically, reevaluate_automatically)
{"continue_on_error", continue_on_error}, attrs ->
Map.put(attrs, :continue_on_error, continue_on_error)
_entry, attrs -> _entry, attrs ->
attrs attrs
end) end)

View file

@ -6,7 +6,14 @@ defmodule Livebook.Notebook.Cell.Code do
# It consists of text content that the user can edit # It consists of text content that the user can edit
# and produces some output once evaluated. # and produces some output once evaluated.
defstruct [:id, :source, :outputs, :disable_formatting, :reevaluate_automatically] defstruct [
:id,
:source,
:outputs,
:disable_formatting,
:reevaluate_automatically,
:continue_on_error
]
alias Livebook.Utils alias Livebook.Utils
alias Livebook.Notebook.Cell alias Livebook.Notebook.Cell
@ -16,7 +23,8 @@ defmodule Livebook.Notebook.Cell.Code do
source: String.t() | :__pruned__, source: String.t() | :__pruned__,
outputs: list(Cell.indexed_output()), outputs: list(Cell.indexed_output()),
disable_formatting: boolean(), disable_formatting: boolean(),
reevaluate_automatically: boolean() reevaluate_automatically: boolean(),
continue_on_error: boolean()
} }
@doc """ @doc """
@ -29,7 +37,8 @@ defmodule Livebook.Notebook.Cell.Code do
source: "", source: "",
outputs: [], outputs: [],
disable_formatting: false, disable_formatting: false,
reevaluate_automatically: false reevaluate_automatically: false,
continue_on_error: false
} }
end end
end end

View file

@ -78,6 +78,7 @@ defprotocol Livebook.Runtime do
dependencies between evaluations and avoids unnecessary reevaluations. dependencies between evaluations and avoids unnecessary reevaluations.
""" """
@type evaluation_response_metadata :: %{ @type evaluation_response_metadata :: %{
errored: boolean(),
evaluation_time_ms: non_neg_integer(), evaluation_time_ms: non_neg_integer(),
code_error: code_error(), code_error: code_error(),
memory_usage: runtime_memory(), memory_usage: runtime_memory(),

View file

@ -466,6 +466,7 @@ defmodule Livebook.Runtime.Evaluator do
output = state.formatter.format_result(result) output = state.formatter.format_result(result)
metadata = %{ metadata = %{
errored: elem(result, 0) == :error,
evaluation_time_ms: evaluation_time_ms, evaluation_time_ms: evaluation_time_ms,
memory_usage: memory(), memory_usage: memory(),
code_error: code_error, code_error: code_error,

View file

@ -88,11 +88,13 @@ defmodule Livebook.Session.Data do
@type cell_eval_info :: %{ @type cell_eval_info :: %{
validity: cell_evaluation_validity(), validity: cell_evaluation_validity(),
status: cell_evaluation_status(), status: cell_evaluation_status(),
errored: boolean(),
snapshot: snapshot(), snapshot: snapshot(),
evaluation_digest: String.t() | nil, evaluation_digest: String.t() | nil,
evaluation_snapshot: snapshot() | nil, evaluation_snapshot: snapshot() | nil,
evaluation_time_ms: integer() | nil, evaluation_time_ms: integer() | nil,
evaluation_start: DateTime.t() | nil, evaluation_start: DateTime.t() | nil,
evaluation_end: DateTime.t() | nil,
evaluation_number: non_neg_integer(), evaluation_number: non_neg_integer(),
outputs_batch_number: non_neg_integer(), outputs_batch_number: non_neg_integer(),
bound_to_input_ids: MapSet.t(input_id()), bound_to_input_ids: MapSet.t(input_id()),
@ -1051,19 +1053,29 @@ defmodule Livebook.Session.Data do
end end
defp finish_cell_evaluation(data_actions, cell, section, metadata) do defp finish_cell_evaluation(data_actions, cell, section, metadata) do
data_actions data_actions =
|> update_cell_eval_info!(cell.id, fn eval_info -> data_actions
%{ |> update_cell_eval_info!(cell.id, fn eval_info ->
eval_info %{
| status: :ready, eval_info
evaluation_time_ms: metadata.evaluation_time_ms, | status: :ready,
identifiers_used: metadata.identifiers_used, errored: metadata.errored,
identifiers_defined: metadata.identifiers_defined, evaluation_time_ms: metadata.evaluation_time_ms,
bound_to_input_ids: eval_info.new_bound_to_input_ids identifiers_used: metadata.identifiers_used,
} identifiers_defined: metadata.identifiers_defined,
end) bound_to_input_ids: eval_info.new_bound_to_input_ids,
|> update_cell_evaluation_snapshot(cell, section) evaluation_end: DateTime.utc_now()
|> set_section_info!(section.id, evaluating_cell_id: nil) }
end)
|> update_cell_evaluation_snapshot(cell, section)
|> set_section_info!(section.id, evaluating_cell_id: nil)
if metadata.errored and not Map.get(cell, :continue_on_error, false) do
data_actions
|> unqueue_child_cells_evaluation(cell)
else
data_actions
end
end end
defp update_cell_evaluation_snapshot({data, _} = data_actions, cell, section) do defp update_cell_evaluation_snapshot({data, _} = data_actions, cell, section) do
@ -1241,7 +1253,8 @@ defmodule Livebook.Session.Data do
data: data, data: data,
# This is a rough estimate, the exact time is measured in the # This is a rough estimate, the exact time is measured in the
# evaluator itself # evaluator itself
evaluation_start: DateTime.utc_now() evaluation_start: DateTime.utc_now(),
evaluation_end: nil
} }
end) end)
|> set_section_info!(section.id, |> set_section_info!(section.id,
@ -1786,9 +1799,11 @@ defmodule Livebook.Session.Data do
%{ %{
validity: :fresh, validity: :fresh,
status: :ready, status: :ready,
errored: false,
evaluation_digest: nil, evaluation_digest: nil,
evaluation_time_ms: nil, evaluation_time_ms: nil,
evaluation_start: nil, evaluation_start: nil,
evaluation_end: nil,
evaluation_number: 0, evaluation_number: 0,
outputs_batch_number: 0, outputs_batch_number: 0,
bound_to_input_ids: MapSet.new(), bound_to_input_ids: MapSet.new(),
@ -1797,7 +1812,8 @@ defmodule Livebook.Session.Data do
identifiers_defined: %{}, identifiers_defined: %{},
snapshot: nil, snapshot: nil,
evaluation_snapshot: nil, evaluation_snapshot: nil,
data: nil data: nil,
reevaluates_automatically: false
} }
end end
@ -1923,6 +1939,7 @@ defmodule Livebook.Session.Data do
data_actions data_actions
|> compute_snapshots() |> compute_snapshots()
|> update_validity() |> update_validity()
|> update_reevaluates_automatically()
# After updating validity there may be new stale cells, so we check # After updating validity there may be new stale cells, so we check
# if any of them is configured for automatic reevaluation # if any of them is configured for automatic reevaluation
|> maybe_queue_reevaluating_cells() |> maybe_queue_reevaluating_cells()
@ -2066,15 +2083,45 @@ defmodule Livebook.Session.Data do
end) end)
end end
defp update_reevaluates_automatically({data, _} = data_actions) do
eval_parents = cell_evaluation_parents(data)
cells_with_section = Notebook.evaluable_cells_with_section(data.notebook)
cell_lookup = for {cell, _section} <- cells_with_section, into: %{}, do: {cell.id, cell}
reduce(data_actions, cells_with_section, fn data_actions, {cell, _section} ->
info = data.cell_infos[cell.id]
# Note that we disable automatic reevaluation if any evaluation
# parent errored (unless continue-on-error is enabled)
reevaluates_automatically =
Map.get(cell, :reevaluate_automatically, false) and
info.eval.validity in [:evaluated, :stale] and
not Enum.any?(eval_parents[cell.id], fn parent_id ->
errored? = data.cell_infos[parent_id].eval.errored
continue_on_error? = Map.get(cell_lookup[parent_id], :continue_on_error, false)
errored? and not continue_on_error?
end)
if reevaluates_automatically == info.eval.reevaluates_automatically do
data_actions
else
update_cell_eval_info!(
data_actions,
cell.id,
&%{&1 | reevaluates_automatically: reevaluates_automatically}
)
end
end)
end
defp maybe_queue_reevaluating_cells({data, _} = data_actions) do defp maybe_queue_reevaluating_cells({data, _} = data_actions) do
cells_to_reevaluate = cells_to_reevaluate =
data.notebook data.notebook
|> Notebook.evaluable_cells_with_section() |> Notebook.evaluable_cells_with_section()
|> Enum.filter(fn {cell, _section} -> |> Enum.filter(fn {cell, _section} ->
info = data.cell_infos[cell.id] info = data.cell_infos[cell.id]
match?(%{status: :ready, validity: :stale, reevaluates_automatically: true}, info.eval)
info.eval.status == :ready and info.eval.validity == :stale and
Map.get(cell, :reevaluate_automatically, false)
end) end)
data_actions data_actions

View file

@ -2031,15 +2031,29 @@ defmodule LivebookWeb.SessionLive do
end end
defp cells_status(cells, data) do defp cells_status(cells, data) do
eval_infos =
for cell <- cells,
Cell.evaluable?(cell),
info = data.cell_infos[cell.id].eval,
do: Map.put(info, :id, cell.id)
most_recent =
eval_infos
|> Enum.filter(& &1.evaluation_end)
|> Enum.max_by(& &1.evaluation_end, DateTime, fn -> nil end)
cond do cond do
evaluating = Enum.find(cells, &evaluating?(&1, data)) -> evaluating = Enum.find(eval_infos, &(&1.status == :evaluating)) ->
{:evaluating, evaluating.id} {:evaluating, evaluating.id}
stale = Enum.find(cells, &stale?(&1, data)) -> most_recent != nil and most_recent.errored ->
{:errored, most_recent.id}
stale = Enum.find(eval_infos, &(&1.validity == :stale)) ->
{:stale, stale.id} {:stale, stale.id}
evaluated = Enum.find(Enum.reverse(cells), &evaluated?(&1, data)) -> most_recent != nil ->
{:evaluated, evaluated.id} {:evaluated, most_recent.id}
true -> true ->
{:fresh, nil} {:fresh, nil}
@ -2055,18 +2069,6 @@ defmodule LivebookWeb.SessionLive do
cells_status(cells, data) cells_status(cells, data)
end end
defp evaluating?(cell, data) do
get_in(data.cell_infos, [cell.id, :eval, :status]) == :evaluating
end
defp stale?(cell, data) do
get_in(data.cell_infos, [cell.id, :eval, :validity]) == :stale
end
defp evaluated?(cell, data) do
get_in(data.cell_infos, [cell.id, :eval, :validity]) == :evaluated
end
defp section_views(sections, data) do defp section_views(sections, data) do
sections sections
|> Enum.map(& &1.name) |> Enum.map(& &1.name)
@ -2142,6 +2144,8 @@ defmodule LivebookWeb.SessionLive do
outputs: cell.outputs, outputs: cell.outputs,
validity: eval_info.validity, validity: eval_info.validity,
status: eval_info.status, status: eval_info.status,
errored: eval_info.errored,
reevaluates_automatically: eval_info.reevaluates_automatically,
evaluation_time_ms: eval_info.evaluation_time_ms, evaluation_time_ms: eval_info.evaluation_time_ms,
evaluation_start: eval_info.evaluation_start, evaluation_start: eval_info.evaluation_start,
evaluation_digest: encode_digest(eval_info.evaluation_digest), evaluation_digest: encode_digest(eval_info.evaluation_digest),

View file

@ -68,6 +68,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
validity={@cell_view.eval.validity} validity={@cell_view.eval.validity}
status={@cell_view.eval.status} status={@cell_view.eval.status}
reevaluate_automatically={@cell_view.reevaluate_automatically} reevaluate_automatically={@cell_view.reevaluate_automatically}
reevaluates_automatically={@cell_view.eval.reevaluates_automatically}
/> />
</:primary> </:primary>
<:secondary> <:secondary>
@ -167,6 +168,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
validity={@cell_view.eval.validity} validity={@cell_view.eval.validity}
status={@cell_view.eval.status} status={@cell_view.eval.status}
reevaluate_automatically={false} reevaluate_automatically={false}
reevaluates_automatically={@cell_view.eval.reevaluates_automatically}
/> />
</:primary> </:primary>
<:secondary> <:secondary>
@ -307,7 +309,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
data-cell-id={@cell_id} data-cell-id={@cell_id}
> >
<%= cond do %> <%= cond do %>
<% @reevaluate_automatically and @validity in [:evaluated, :stale] -> %> <% @reevaluates_automatically -> %>
<.remix_icon icon="check-line" class="text-xl" /> <.remix_icon icon="check-line" class="text-xl" />
<span class="text-sm font-medium">Reevaluates automatically</span> <span class="text-sm font-medium">Reevaluates automatically</span>
<% @validity == :evaluated -> %> <% @validity == :evaluated -> %>
@ -653,7 +655,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
defp cell_status(%{cell_view: %{eval: %{validity: :evaluated}}} = assigns) do defp cell_status(%{cell_view: %{eval: %{validity: :evaluated}}} = assigns) do
~H""" ~H"""
<.status_indicator <.status_indicator
circle_class="bg-green-bright-400" circle_class={if(@cell_view.eval.errored, do: "bg-red-400", else: "bg-green-bright-400")}
change_indicator={true} change_indicator={true}
tooltip={evaluated_label(@cell_view.eval.evaluation_time_ms)} tooltip={evaluated_label(@cell_view.eval.evaluation_time_ms)}
> >

View file

@ -12,6 +12,7 @@ defmodule LivebookWeb.SessionLive.CodeCellSettingsComponent do
|> assign(assigns) |> assign(assigns)
|> assign_new(:disable_formatting, fn -> cell.disable_formatting end) |> assign_new(:disable_formatting, fn -> cell.disable_formatting end)
|> assign_new(:reevaluate_automatically, fn -> cell.reevaluate_automatically end) |> assign_new(:reevaluate_automatically, fn -> cell.reevaluate_automatically end)
|> assign_new(:continue_on_error, fn -> cell.continue_on_error end)
{:ok, socket} {:ok, socket}
end end
@ -38,6 +39,13 @@ defmodule LivebookWeb.SessionLive.CodeCellSettingsComponent do
checked={@reevaluate_automatically} checked={@reevaluate_automatically}
/> />
</div> </div>
<div class="w-full flex-col space-y-6 mt-4">
<.switch_checkbox
name="continue_on_error"
label="Continue on error"
checked={@continue_on_error}
/>
</div>
<div class="mt-8 flex justify-begin space-x-2"> <div class="mt-8 flex justify-begin space-x-2">
<button class="button-base button-blue" type="submit"> <button class="button-base button-blue" type="submit">
Save Save
@ -54,16 +62,19 @@ defmodule LivebookWeb.SessionLive.CodeCellSettingsComponent do
"save", "save",
%{ %{
"enable_formatting" => enable_formatting, "enable_formatting" => enable_formatting,
"reevaluate_automatically" => reevaluate_automatically "reevaluate_automatically" => reevaluate_automatically,
"continue_on_error" => continue_on_error
}, },
socket socket
) do ) do
disable_formatting = enable_formatting == "false" disable_formatting = enable_formatting == "false"
reevaluate_automatically = reevaluate_automatically == "true" reevaluate_automatically = reevaluate_automatically == "true"
continue_on_error = continue_on_error == "true"
Session.set_cell_attributes(socket.assigns.session.pid, socket.assigns.cell.id, %{ Session.set_cell_attributes(socket.assigns.session.pid, socket.assigns.cell.id, %{
disable_formatting: disable_formatting, disable_formatting: disable_formatting,
reevaluate_automatically: reevaluate_automatically reevaluate_automatically: reevaluate_automatically,
continue_on_error: continue_on_error
}) })
{:noreply, push_patch(socket, to: socket.assigns.return_to)} {:noreply, push_patch(socket, to: socket.assigns.return_to)}

View file

@ -190,6 +190,21 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
""" """
end end
defp global_status(%{status: :errored} = assigns) do
~H"""
<span class="tooltip left" data-tooltip="Go to last evaluated cell">
<button
class="border-red-300 icon-button icon-outlined-button hover:bg-red-50 focus:bg-red-50"
aria-label="go to last evaluated cell"
data-el-focus-cell-button
data-target={@cell_id}
>
<.remix_icon icon="loader-3-line" class="text-xl text-red-400" />
</button>
</span>
"""
end
defp global_status(%{status: :stale} = assigns) do defp global_status(%{status: :stale} = assigns) do
~H""" ~H"""
<span class="tooltip left" data-tooltip="Go to first stale cell"> <span class="tooltip left" data-tooltip="Go to first stale cell">

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" id="svg2" xml:space="preserve" width="823.87854" height="901.65137" viewBox="0 0 823.87854 901.65133" sodipodi:docname="favicon-errored.svg" inkscape:version="1.2.2 (b0a84865, 2022-12-01)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>
image/svg+xml
</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs6">
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath20">
<path d="M 0,805.333 H 3800 V 0 H 0 Z" id="path18"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath148">
<path d="M 353.619,544.464 H 573.136 V 440.053 H 353.619 Z" id="path146"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath172">
<path d="m 351.9,352.883 h 95.147 V 122.614 H 351.9 Z" id="path170"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath1292">
<path id="path1294" style="fill:#000000;fill-opacity:1;stroke-width:7.50045;stroke-linecap:round;stroke-linejoin:round" d="m 202.05067,751.83844 c -9.972,0 -18,-8.028 -18,-18 V -10.124963 c 0,-9.971978 8.028,-17.999978 18,-17.999978 h 710.98537 c 9.972,0 18,8.028 18,17.999978 V 733.83844 c 0,9.972 -8.028,18 -18,18 z M 696.25088,372.24907 a 187.35425,187.35425 0 0 0 0.2417,0 A 187.35425,187.35425 0 0 0 883.84609,184.89408 187.35425,187.35425 0 0 0 696.49258,-2.4594376 187.35425,187.35425 0 0 0 509.1376,184.89408 187.35425,187.35425 0 0 0 696.25088,372.24907 Z"/>
</clipPath>
</defs>
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="1027" id="namedview4" showgrid="false" inkscape:zoom="0.48408519" inkscape:cx="480.28736" inkscape:cy="837.66248" inkscape:window-x="584" inkscape:window-y="25" inkscape:window-maximized="0" inkscape:current-layer="g10" inkscape:document-rotation="0" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1"/>
<g id="g10" inkscape:groupmode="layer" inkscape:label="editable_Livebook_Logo" transform="matrix(1.3333333,0,0,-1.3333333,-271.62159,986.33208)">
<g id="g14" clip-path="url(#clipPath1292)">
<g id="g16" clip-path="url(#clipPath20)">
<g id="g118" transform="translate(425.3255,680.1453)">
<path d="M 0,0 C 27.577,33.093 44.051,35.111 44.303,34.988 53.563,33.872 54.75,25.563 54.75,25.563 L 48.815,7.759 36.397,-25.372 c -12.991,2.166 -50.193,4.688 -52.949,3.31 z" style="fill:#825161;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path120"/>
</g>
<g id="g122" transform="translate(465.032,662.4954)">
<path d="m 0,0 c 0,0 1.103,3.31 3.309,9.928 0.149,0.446 -33.093,5.515 -55.154,-5.515 0,0 33.092,2.206 51.845,-4.413" style="fill:#563642;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path124"/>
</g>
<g id="g126" transform="translate(416.4956,669.1141)">
<path d="m 0,0 5.638,59.34 c -1.225,10.155 -7.844,14.568 -18.421,8.598 -1.514,-0.854 -3.384,-2.171 -4.941,-3.407 -37.828,-30.007 -44.168,-81.382 -45.215,-106.367 0.017,-0.925 -0.012,-5.787 -0.051,-6.7 -0.989,-51.846 68.077,-90.357 87.258,-109.207 -3.976,7.409 -33.71,32.402 -37.505,50.743 -2.915,14.088 0.033,25.5 4.619,34.123 5.83,10.959 15.579,19.332 27.085,23.993 29.794,12.071 59.031,10.045 84.121,5.863 38.691,-6.448 69.005,-22.321 70.537,-23.132 0.041,-0.022 0.061,-0.033 0.061,-0.033 -0.028,0.014 1.707,6.516 0,9.928 -2.206,4.413 -7.109,6.373 -7.868,6.778 C 153.087,-42.987 98.646,-13.885 51.183,-3.768 54.052,-4.412 25.371,2.207 0,0" style="fill:#825161;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path128"/>
</g>
<g id="g130" transform="translate(525.7024,616.1653)">
<path d="m 0,0 c -25.09,4.182 -50.834,4.135 -80.628,-7.936 -11.505,-4.661 -21.747,-14.412 -27.577,-25.371 -4.587,-8.622 -6.517,-17.435 -3.208,-33.982 2.632,-13.159 8.825,-26.474 14.341,-35.299 22.621,-18.788 30.59,-16.362 56.257,-31.99 -7.706,37.317 0.852,89.735 89.315,110.745 0.087,0.021 7.234,1.567 7.516,1.687 3.934,1.668 6.669,5.099 7.922,8.931 C 63.067,-12.751 39.14,-6.523 0,0" style="fill:#c4adb5;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path132"/>
</g>
<g id="g134" transform="translate(589.6821,607.3406)">
<path d="m 0,0 c -13.237,4.413 -31.99,9.928 -57.708,16.292 -29.298,7.249 -64.537,10.376 -90.107,0.254 0,0 -58.464,-16.546 -38.609,-77.216 l 3.31,-13.237 c 0,0 6.581,0.003 13.812,-5.112 -1.147,3.495 -5.422,17.002 -6.132,20.567 -3.298,16.556 0.033,25.515 4.604,34.142 5.81,10.966 15.527,19.344 26.994,24.008 6.942,2.824 13.999,4.993 21.056,6.608 4.301,0.984 8.665,1.64 13.064,1.989 21.894,1.741 50.539,1.716 107.51,-12.707 l 0.154,0.464 c 0.948,-0.273 1.421,-0.421 1.421,-0.421 C -0.589,-4.391 0.177,-0.059 0,0" style="fill:#4c313b;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path136"/>
</g>
<g id="g138" transform="translate(354.1892,303.2845)">
<path d="m 0,0 c -0.044,-10.08 2.465,-20.009 7.354,-28.825 9.226,-16.633 29.838,-42.629 75.711,-72.288 52.438,-33.904 146.912,-63.851 146.912,-63.851 l 13.238,291.422 c 0,0 -243.836,115.46 -243.836,197.646 C -0.621,326.15 0.245,55.466 0,0" style="fill:#dbd8d5;fill-opacity:1;fill-rule:evenodd;stroke:none" id="path140"/>
</g>
<g id="g142">
<g id="g144"/>
<g id="g156">
<g clip-path="url(#clipPath148)" opacity="0.25" id="g154">
<g transform="translate(359.1348,529.0208)" id="g152">
<path d="m 0,0 214.001,-88.248 v 1.103 L -5.516,15.443 V 2.206 c 0,0 0.102,-0.038 5.516,-2.206" style="fill:#36272b;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path150"/>
</g>
</g>
</g>
</g>
<g id="g158" transform="translate(293.3445,165.7054)">
<path d="m 0,0 c -3.146,1.255 -5.206,4.305 -5.196,7.692 l 0.424,376.462 c -0.11,2.985 0.066,7.804 5.48,5.636 L 262.142,286.099 c 0,-3.309 0.015,-2.127 0,-5.516 l -0.648,-371.921 c -0.021,-4.584 0.648,-7.544 0.648,-10.854 C 262.142,-102.82 0,0 0,0" style="fill:#c24063;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path160"/>
</g>
<g id="g162" transform="translate(555.4861,63.5135)">
<path d="m 0,0 260.943,102.192 c 3.146,1.255 5.206,4.305 5.196,7.692 l -0.424,376.462 c 0.061,1.642 -0.271,3.506 -1.483,4.489 -0.99,0.804 -2.569,1.019 -5.004,0.043 L 0,388.291 c 0,-4.413 -0.015,-2.127 0,-5.516 L -0.551,10.854 C -0.525,5.032 0,3.31 0,0" style="fill:#ff87a7;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path164"/>
</g>
<g id="g166">
<g id="g168"/>
<g id="g180">
<g clip-path="url(#clipPath172)" opacity="0.25" id="g178">
<g transform="translate(396.6399,125.2874)" id="g176">
<path d="m 0,0 c 0,0 25.629,-13.707 41.918,20.958 16.816,35.79 11.031,138.991 -39.073,206.638 0,0 33.557,-168.029 -47.585,-209.748 z" style="fill:#36272b;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path174"/>
</g>
</g>
</g>
</g>
<g id="g182" transform="translate(235.1873,329.8465)">
<path d="m 0,0 -6.929,-91.382 c -1.889,-24.9 6.023,-49.032 22.274,-67.955 16.147,-18.923 39.281,-30.415 63.952,-32.286 5.208,-0.395 10.484,-0.359 15.781,0.124 44.579,4.068 80.894,42.703 84.471,89.866 l 3.337,44.017 C 187.631,4.953 156.231,63.07 102.889,90.441 59.026,112.964 14.225,107.04 -30.368,72.879 -9.406,54.897 2.1,27.688 0,0" style="fill:#1f2728;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path184"/>
</g>
<g id="g186" transform="translate(311.2022,170.5091)">
<path d="m 0,0 c -16.25,18.924 -24.162,43.056 -22.274,67.956 l 6.93,91.381 c 2.099,27.689 -9.406,54.898 -30.368,72.879 16.567,12.692 33.162,21.478 49.728,26.364 -36.867,10.985 -74.257,2.169 -111.502,-26.364 20.962,-17.981 32.468,-45.19 30.368,-72.879 l -6.929,-91.381 c -1.889,-24.9 6.023,-49.032 22.274,-67.956 16.147,-18.922 39.281,-30.415 63.952,-32.286 5.208,-0.395 10.484,-0.359 15.781,0.125 7.571,0.691 14.895,2.4 21.87,4.958 C 24.539,-21.832 10.724,-12.567 0,0" style="fill:#2f393d;fill-opacity:1;fill-rule:nonzero;stroke:none" id="path188"/>
</g>
</g>
</g>
<circle style="fill:#e97579;fill-opacity:1;stroke-width:10.4152;stroke-linecap:round;stroke-linejoin:round" id="path1157" cx="698.31262" cy="-185.48764" transform="scale(1,-1)" r="114.5602"/>
<path fill="none" d="m -530.12015,778.48595 h 18 v -18 h -18 z" id="path2" style="stroke-width:0.75"/>
<path d="m -522.62015,767.10695 6.894,6.89475 1.06125,-1.0605 -7.95525,-7.95525 -4.773,4.773 1.0605,1.0605 z" id="path4" style="stroke-width:0.75"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -29,6 +29,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
Notebook.Cell.new(:code) Notebook.Cell.new(:code)
| disable_formatting: true, | disable_formatting: true,
reevaluate_automatically: true, reevaluate_automatically: true,
continue_on_error: true,
source: """ source: """
Enum.to_list(1..10)\ Enum.to_list(1..10)\
""" """
@ -101,7 +102,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
$x_{i} + y_{i}$ $x_{i} + y_{i}$
<!-- livebook:{"disable_formatting":true,"reevaluate_automatically":true} --> <!-- livebook:{"continue_on_error":true,"disable_formatting":true,"reevaluate_automatically":true} -->
```elixir ```elixir
Enum.to_list(1..10) Enum.to_list(1..10)

View file

@ -19,7 +19,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
$x_{i} + y_{i}$ $x_{i} + y_{i}$
<!-- livebook:{"disable_formatting":true,"reevaluate_automatically":true} --> <!-- livebook:{"continue_on_error":true,"disable_formatting":true,"reevaluate_automatically":true} -->
```elixir ```elixir
Enum.to_list(1..10) Enum.to_list(1..10)
@ -80,6 +80,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do
%Cell.Code{ %Cell.Code{
disable_formatting: true, disable_formatting: true,
reevaluate_automatically: true, reevaluate_automatically: true,
continue_on_error: true,
source: """ source: """
Enum.to_list(1..10)\ Enum.to_list(1..10)\
""" """

View file

@ -15,7 +15,14 @@ defmodule Livebook.Session.DataTest do
defp eval_meta(opts \\ []) do defp eval_meta(opts \\ []) do
uses = opts[:uses] || [] uses = opts[:uses] || []
defines = opts[:defines] || %{} defines = opts[:defines] || %{}
%{evaluation_time_ms: 10, identifiers_used: uses, identifiers_defined: defines} errored = Keyword.get(opts, :errored, false)
%{
errored: errored,
evaluation_time_ms: 10,
identifiers_used: uses,
identifiers_defined: defines
}
end end
describe "new/1" do describe "new/1" do
@ -2038,8 +2045,7 @@ defmodule Livebook.Session.DataTest do
]) ])
operation = operation =
{:add_cell_evaluation_response, @cid, "c1", @eval_resp, {:add_cell_evaluation_response, @cid, "c1", @eval_resp, eval_meta(defines: %{"c1" => 1})}
eval_meta(%{defines: %{"c1" => 1}})}
assert {:ok, assert {:ok,
%{ %{
@ -2173,6 +2179,98 @@ defmodule Livebook.Session.DataTest do
}, _actions} = Data.apply_operation(data, operation) }, _actions} = Data.apply_operation(data, operation)
end end
test "unqueues child cells if the evaluation errored" do
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
{:insert_cell, @cid, "s1", 0, :code, "c1", %{}},
{:insert_cell, @cid, "s1", 1, :code, "c2", %{}},
{:insert_section, @cid, 1, "s2"},
{:insert_cell, @cid, "s2", 0, :code, "c3", %{}},
{:insert_cell, @cid, "s2", 1, :code, "c4", %{}},
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1", "c2", "c3", "c4"]}
])
operation =
{:add_cell_evaluation_response, @cid, "c1", @eval_resp, eval_meta(errored: true)}
assert {:ok,
%{
cell_infos: %{
"c2" => %{eval: %{status: :ready, validity: :fresh}},
"c3" => %{eval: %{status: :ready, validity: :fresh}},
"c4" => %{eval: %{status: :ready, validity: :fresh}}
},
section_infos: %{
"s1" => %{evaluating_cell_id: nil},
"s2" => %{evaluating_cell_id: nil}
}
}, []} = Data.apply_operation(data, operation)
end
test "disables child cells automatic reevaluation if the evaluation errored" do
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
{:insert_cell, @cid, "s1", 0, :code, "c1", %{}},
{:insert_cell, @cid, "s1", 1, :code, "c2", %{}},
{:insert_cell, @cid, "s1", 2, :code, "c3", %{}},
{:set_cell_attributes, @cid, "c3", %{reevaluate_automatically: true}},
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup", "c1", "c2", "c3"],
uses: %{"c2" => ["c1"], "c3" => ["c2"]}
),
{:queue_cells_evaluation, @cid, ["c1"]}
])
operation =
{:add_cell_evaluation_response, @cid, "c1", @eval_resp, eval_meta(errored: true)}
assert {:ok,
%{
cell_infos: %{
"c2" => %{eval: %{status: :ready}},
"c3" => %{eval: %{status: :ready, reevaluates_automatically: false}}
},
section_infos: %{
"s1" => %{evaluating_cell_id: nil}
}
}, []} = Data.apply_operation(data, operation)
end
test "re-enables child cells automatic reevaluation if errored evaluation is fixed" do
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
{:insert_cell, @cid, "s1", 0, :code, "c1", %{}},
{:insert_cell, @cid, "s1", 1, :code, "c2", %{}},
{:insert_cell, @cid, "s1", 2, :code, "c3", %{}},
{:set_cell_attributes, @cid, "c3", %{reevaluate_automatically: true}},
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup", "c1", "c2", "c3"],
uses: %{"c2" => ["c1"], "c3" => ["c2"]}
),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", @eval_resp, eval_meta(errored: true)},
{:queue_cells_evaluation, @cid, ["c1"]}
])
operation = {:add_cell_evaluation_response, @cid, "c1", @eval_resp, eval_meta()}
assert {:ok,
%{
cell_infos: %{
"c2" => %{eval: %{status: :evaluating}},
"c3" => %{eval: %{status: :queued, reevaluates_automatically: true}}
},
section_infos: %{
"s1" => %{evaluating_cell_id: "c2"}
}
}, _actions} = Data.apply_operation(data, operation)
end
test "if bound input value changes during cell evaluation, the cell is marked as stale afterwards" do test "if bound input value changes during cell evaluation, the cell is marked as stale afterwards" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"} input = %{id: "i1", type: :text, label: "Text", default: "hey"}

View file

@ -7,7 +7,12 @@ defmodule Livebook.SessionTest do
alias Livebook.Notebook.{Section, Cell} alias Livebook.Notebook.{Section, Cell}
alias Livebook.Session.Data alias Livebook.Session.Data
@eval_meta %{evaluation_time_ms: 10, identifiers_used: [], identifiers_defined: %{}} @eval_meta %{
errored: false,
evaluation_time_ms: 10,
identifiers_used: [],
identifiers_defined: %{}
}
setup do setup do
session = start_session() session = start_session()