this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Delete Construct and unused data structures

garrison b185c747 06abf0a6

-3200
-772
lib/construct/scheduler.ex
··· 1 - defmodule Hobbes.Construct.Scheduler do 2 - use GenServer 3 - 4 - alias Hobbes.Construct.SimLog 5 - alias Hobbes.Construct.Scheduler.{ProcStore, ProcQueue, ProcRegistry, FileStore} 6 - alias Hobbes.Construct.Scheduler.ProcStore.ProcState 7 - 8 - defmodule Spawn do 9 - @enforce_keys [:module, :function, :args, :link, :parent_resume] 10 - defstruct @enforce_keys 11 - end 12 - 13 - defmodule Resume do 14 - @enforce_keys [:ref, :pid, :value] 15 - defstruct @enforce_keys 16 - end 17 - 18 - defmodule Timeout do 19 - @enforce_keys [:pid, :ref] 20 - defstruct @enforce_keys 21 - end 22 - 23 - defmodule Send do 24 - @enforce_keys [:dest, :message] 25 - defstruct @enforce_keys 26 - end 27 - 28 - defmodule Exit do 29 - @enforce_keys [:from_pid, :to_pid, :reason] 30 - defstruct @enforce_keys 31 - end 32 - 33 - defmodule StartNode do 34 - @enforce_keys [:name] 35 - defstruct @enforce_keys 36 - end 37 - 38 - defmodule Node do 39 - @type t :: %__MODULE__{ 40 - pid: pid | nil, 41 - status: :running | :stopped, 42 - name: atom, 43 - app_module: module, 44 - args: term, 45 - } 46 - @enforce_keys [:pid, :status, :name, :app_module, :args] 47 - defstruct @enforce_keys 48 - end 49 - 50 - defmodule State do 51 - @type t :: %__MODULE__{ 52 - clock: non_neg_integer, 53 - current: pid | nil, 54 - 55 - nodes: [{atom, Node.t}], 56 - proc_store: ProcStore.t, 57 - proc_queue: ProcQueue.t, 58 - proc_registry: ProcRegistry.t, 59 - file_stores: %{atom => FileStore.t}, 60 - 61 - log_server_pid: pid, 62 - 63 - resumes_without_send: non_neg_integer, 64 - } 65 - 66 - defstruct [ 67 - clock: 0, 68 - 69 - nodes: [], 70 - current: nil, 71 - proc_store: nil, 72 - proc_queue: nil, 73 - proc_registry: nil, 74 - file_stores: %{}, 75 - 76 - log_server_pid: nil, 77 - resumes_without_send: 0, 78 - ] 79 - end 80 - 81 - @spec start_link(term) :: GenServer.on_start 82 - def start_link(seed) when is_integer(seed) do 83 - :rand.seed(:exsss, seed) 84 - scheduler_seed = random_seed() 85 - 86 - {:ok, scheduler_pid} = GenServer.start_link(__MODULE__, %{initial_pid: self(), seed: scheduler_seed}) 87 - Hobbes.Construct.SimServer.set_scheduler_pid(scheduler_pid) 88 - 89 - {:ok, scheduler_pid} 90 - end 91 - 92 - @spec yield(term) :: :ok 93 - def yield(scheduler, delay \\ 0) do 94 - resume = %Resume{ref: make_ref(), pid: self(), value: nil} 95 - GenServer.cast(scheduler, {:queue_task, self(), delay, resume}) 96 - GenServer.cast(scheduler, {:yield, self()}) 97 - 98 - nil = await_resume(resume) 99 - :ok 100 - end 101 - 102 - @spec yield_until_message(pid, function, non_neg_integer | :infinity) :: :ok 103 - def yield_until_message(scheduler, check_fun, timeout_ms) 104 - when is_function(check_fun) and ((is_integer(timeout_ms) and timeout_ms >= 0) or timeout_ms == :infinity) do 105 - resume = %Resume{ref: make_ref(), pid: self(), value: nil} 106 - 107 - GenServer.cast(scheduler, {:await_message, self(), check_fun, timeout_ms, resume}) 108 - GenServer.cast(scheduler, {:yield, self()}) 109 - 110 - nil = await_resume(resume) 111 - :ok 112 - end 113 - 114 - @spec spawn_and_yield(term, :link | :nolink, module, atom, [term]) :: pid 115 - def spawn_and_yield(scheduler, link, module, function, args) when link in [:link, :nolink] do 116 - resume = %Resume{ref: make_ref(), pid: self(), value: nil} 117 - spawn = %Spawn{module: module, function: function, args: args, parent_resume: resume, link: link == :link} 118 - 119 - GenServer.cast(scheduler, {:queue_task, self(), 0, spawn}) 120 - GenServer.cast(scheduler, {:yield, self()}) 121 - 122 - spawned_pid = await_resume(resume) 123 - spawned_pid 124 - end 125 - 126 - defp await_resume(%Resume{ref: ref} = _resume) do 127 - receive do 128 - {:resume, ^ref, value} -> value 129 - {:timeout, ^ref} -> exit(:timeout) 130 - end 131 - end 132 - 133 - @spec send(term, pid, term) :: :ok 134 - def send(scheduler, dest, message, delay \\ 0) do 135 - send = %Send{dest: dest, message: message} 136 - GenServer.cast(scheduler, {:queue_task, self(), delay, send}) 137 - :ok 138 - end 139 - 140 - @spec exit(term, pid, term) :: :ok 141 - def exit(scheduler, to_pid, reason) do 142 - exit = %Exit{from_pid: self(), to_pid: to_pid, reason: reason} 143 - GenServer.cast(scheduler, {:queue_task, self(), 0, exit}) 144 - :ok 145 - end 146 - 147 - @spec start_node(pid, atom, module, term) :: :ok 148 - def start_node(scheduler, name, app_module, args) when is_atom(name) and is_atom(app_module) do 149 - GenServer.call(scheduler, {:create_node, name, app_module, args}) 150 - :ok 151 - end 152 - 153 - @spec restart_node(pid, atom, non_neg_integer) :: :ok | {:error, :node_not_found | :node_stopped} 154 - def restart_node(scheduler, name, delay) when is_atom(name) do 155 - GenServer.call(scheduler, {:restart_node, name, delay}) 156 - end 157 - 158 - @spec list_nodes(pid) :: [atom] 159 - def list_nodes(scheduler) do 160 - GenServer.call(scheduler, :list_nodes) 161 - end 162 - 163 - def get_current_node(scheduler) do 164 - GenServer.call(scheduler, {:get_current_node, self()}) 165 - end 166 - 167 - @spec monitor(pid, pid) :: reference 168 - def monitor(scheduler, target_pid) do 169 - ref = make_ref() 170 - :ok = GenServer.call(scheduler, {:monitor, self(), target_pid, ref}) 171 - 172 - ref 173 - end 174 - 175 - @spec alias(pid) :: reference 176 - def alias(scheduler) do 177 - GenServer.call(scheduler, {:alias, self()}) 178 - end 179 - 180 - @spec unalias(pid, reference) :: boolean 181 - def unalias(scheduler, alias) when is_reference(alias) do 182 - GenServer.call(scheduler, {:unalias, self(), alias}) 183 - end 184 - 185 - @spec set_process_flag(pid, :trap_exit, boolean) :: :ok 186 - def set_process_flag(scheduler, :trap_exit = flag, value) when is_boolean(value) do 187 - {:ok, old_value} = GenServer.call(scheduler, {:set_process_flag, self(), flag, value}) 188 - old_value 189 - end 190 - 191 - def register_process(scheduler, pid, name) when is_pid(pid) and is_atom(name) do 192 - GenServer.call(scheduler, {:register_process, self(), pid, name}) 193 - end 194 - 195 - def whereis(scheduler, name) when is_atom(name) do 196 - GenServer.call(scheduler, {:whereis, self(), name}) 197 - end 198 - 199 - @spec get_file_store(pid) :: FileStore.t 200 - def get_file_store(scheduler) do 201 - GenServer.call(scheduler, {:get_file_store, self()}) 202 - end 203 - 204 - @spec current_time(term, pid) :: non_neg_integer 205 - def current_time(scheduler, for_pid \\ self()) when is_pid(for_pid) do 206 - GenServer.call(scheduler, {:current_time, for_pid}) 207 - end 208 - 209 - @spec configure_log_server(term, pid) :: :ok 210 - def configure_log_server(scheduler, log_server_pid) do 211 - GenServer.call(scheduler, {:configure_log_server, log_server_pid}) 212 - end 213 - 214 - @spec get_log(term) :: term 215 - def get_log(scheduler) do 216 - # We run the get_log through the scheduler to ensure 217 - # that all log messages from the scheduler process (which are cast) 218 - # have been processed first (messages are always received in order) 219 - GenServer.call(scheduler, :get_log) 220 - end 221 - 222 - def init(%{initial_pid: initial_pid, seed: seed}) do 223 - Process.flag(:trap_exit, true) 224 - :rand.seed(:exsss, seed) 225 - 226 - state = %State{ 227 - proc_store: ProcStore.new(), 228 - proc_queue: ProcQueue.new(), 229 - proc_registry: ProcRegistry.new(), 230 - # TODO: spawn per node instead 231 - file_stores: %{nonode: FileStore.new()}, 232 - } 233 - ProcStore.add_process(state.proc_store, initial_pid, :nonode) 234 - 235 - state = set_current_process(state, initial_pid) 236 - {:ok, state} 237 - end 238 - 239 - def handle_call({:alias, pid}, _from, %State{} = state) do 240 - alias = make_ref() 241 - ProcStore.add_alias(state.proc_store, pid, alias) 242 - {:reply, alias, state} 243 - end 244 - 245 - def handle_call({:unalias, pid, alias}, _from, %State{} = state) do 246 - case ProcStore.remove_alias(state.proc_store, pid, alias) do 247 - :ok -> {:reply, true, state} 248 - {:error, _err} -> {:reply, false, state} 249 - end 250 - end 251 - 252 - def handle_call({:set_process_flag, pid, flag, value}, _from, %State{} = state) do 253 - {:ok, old_value} = ProcStore.set_flag(state.proc_store, pid, flag, value) 254 - {:reply, {:ok, old_value}, state} 255 - end 256 - 257 - def handle_call({:register_process, from_pid, pid, name}, _from, %State{} = state) do 258 - # TODO: ensure name is valid? (not nil/true/false/:undefined) 259 - {:ok, %ProcState{node: from_node}} = ProcStore.fetch_state(state.proc_store, from_pid) 260 - {:ok, %ProcState{node: node}} = ProcStore.fetch_state(state.proc_store, pid) 261 - 262 - case node == from_node do 263 - true -> 264 - case ProcRegistry.register(state.proc_registry, pid, name, node) do 265 - :ok -> {:reply, :ok, state} 266 - {:error, _error} -> {:reply, :error, state} 267 - end 268 - 269 - false -> {:reply, :error, state} 270 - end 271 - end 272 - 273 - def handle_call({:whereis, from_pid, name}, _from, %State{} = state) do 274 - {:ok, %ProcState{node: node}} = ProcStore.fetch_state(state.proc_store, from_pid) 275 - 276 - case ProcRegistry.whereis(state.proc_registry, name, node) do 277 - {:ok, pid} -> {:reply, pid, state} 278 - {:error, :not_found} -> {:reply, nil, state} 279 - end 280 - end 281 - 282 - def handle_call({:get_file_store, pid}, _from, %State{} = state) when is_pid(pid) do 283 - {:ok, %ProcState{node: node}} = ProcStore.fetch_state(state.proc_store, pid) 284 - 285 - case Map.fetch(state.file_stores, node) do 286 - {:ok, file_store} -> 287 - {:reply, file_store, state} 288 - :error -> 289 - file_store = FileStore.new() 290 - state = %{state | file_stores: Map.put(state.file_stores, node, file_store)} 291 - {:reply, file_store, state} 292 - end 293 - end 294 - 295 - def handle_call({:current_time, for_pid}, _from, %State{} = state) when is_pid(for_pid) do 296 - # TODO: maybe we should store clock in microseconds? 297 - {:reply, state.clock * 1000, state} 298 - end 299 - 300 - def handle_call({:configure_log_server, log_server_pid}, _from, %State{} = state) do 301 - state = %{state | log_server_pid: log_server_pid} 302 - {:reply, :ok, state} 303 - end 304 - 305 - def handle_call(:get_log, _from, %State{} = state) do 306 - log = SimLog.get_log(state.log_server_pid) 307 - {:reply, log, state} 308 - end 309 - 310 - def handle_call({:monitor, pid, target_pid, ref}, _from, %State{} = state) when is_pid(pid) and is_pid(target_pid) and is_reference(ref) do 311 - case ProcStore.fetch_state(state.proc_store, target_pid) do 312 - {:ok, _proc} -> 313 - ProcStore.add_monitor(state.proc_store, pid, target_pid, ref) 314 - {:reply, :ok, state} 315 - 316 - :error -> 317 - send pid, {:DOWN, ref, :process, target_pid, :noproc} 318 - {:reply, :ok, state} 319 - end 320 - end 321 - 322 - def handle_call({:create_node, name, app_module, args}, _from, %State{} = state) when is_atom(name) and is_atom(app_module) do 323 - node = %Node{pid: nil, status: :stopped, name: name, app_module: app_module, args: args} 324 - state = %{state | nodes: [{node.name, node} | state.nodes]} 325 - 326 - queue_task(state, state.clock, %StartNode{name: node.name}) 327 - 328 - {:reply, :ok, state} 329 - end 330 - 331 - def handle_call({:restart_node, name, delay}, _from, %State{} = state) when is_atom(name) and is_integer(delay) do 332 - case List.keyfind(state.nodes, name, 0) do 333 - {^name, %Node{status: :running} = node} -> 334 - :ok = kill_node(state, node) 335 - 336 - node = %{node | pid: nil, status: :stopped} 337 - state = %{state | nodes: List.keyreplace(state.nodes, name, 0, {name, node})} 338 - 339 - start_node = %StartNode{name: node.name} 340 - state = queue_task(state, state.clock + delay, start_node) 341 - 342 - {:reply, :ok, state} 343 - 344 - {^name, %Node{status: :stopped}} -> {:reply, {:error, :node_stopped}, state} 345 - [] -> {:reply, {:error, :node_not_found}, state} 346 - end 347 - end 348 - 349 - def handle_call(:list_nodes, _from, %State{} = state) do 350 - nodes = 351 - state.nodes 352 - |> Enum.map(fn {_name, %Node{} = node} -> node.name end) 353 - |> Enum.reverse() 354 - {:reply, nodes, state} 355 - end 356 - 357 - def handle_call({:get_current_node, pid}, _from, %State{} = state) do 358 - {:ok, %ProcState{node: node}} = ProcStore.fetch_state(state.proc_store, pid) 359 - {:reply, node, state} 360 - end 361 - 362 - def handle_cast({:queue_task, pid, delay, %Send{} = send}, %State{} = state) when is_pid(pid) and is_integer(delay) do 363 - delay = 364 - case send.dest == pid do 365 - true -> delay 366 - # Minimum 10ms of latency for all messages not sent to self() 367 - # TODO: vary this based on whether processes are on the same node 368 - false -> max(delay, 10) 369 - end 370 - 371 - case delay do 372 - 0 -> 373 - log(state, {:send_now, state.clock, send.dest, send.message}) 374 - {:noreply, send_message(state, send)} 375 - 376 - delay when delay > 0 -> 377 - {:noreply, queue_task(state, state.clock + delay, send)} 378 - end 379 - end 380 - 381 - def handle_cast({:queue_task, pid, delay, task}, %State{} = state) when is_pid(pid) and is_integer(delay) do 382 - {:noreply, queue_task(state, state.clock + delay, task)} 383 - end 384 - 385 - def handle_cast({:await_message, pid, check_fun, timeout_ms, %Resume{} = resume}, %State{} = state) when is_pid(pid) and is_function(check_fun) do 386 - # Add new timeout if needed 387 - timeout_key = 388 - case timeout_ms do 389 - :infinity -> 390 - nil 391 - timeout_ms -> 392 - {:ok, key} = ProcQueue.enqueue(state.proc_queue, state.clock + timeout_ms, %Timeout{pid: pid, ref: resume.ref}) 393 - key 394 - end 395 - 396 - ProcStore.track_await(state.proc_store, pid, check_fun, resume, timeout_key) 397 - {:noreply, state} 398 - end 399 - 400 - def handle_cast({:yield, pid}, %State{} = state) when is_pid(pid) do 401 - case state.current do 402 - ^pid -> :noop 403 - end 404 - {:noreply, %{state | current: nil} |> perform_next_task()} 405 - end 406 - 407 - def handle_info({:EXIT, pid, reason} = message, %State{} = state) do 408 - case state.current do 409 - ^pid -> 410 - state = clean_up_dead_process(state, pid, reason) 411 - {:noreply, %{state | current: nil} |> perform_next_task()} 412 - 413 - _ -> 414 - raise """ 415 - Received invalid EXIT message: #{inspect(message)} 416 - 417 - Scheduler state: 418 - 419 - #{inspect(state, pretty: true)} 420 - """ 421 - end 422 - end 423 - 424 - defp queue_task(%State{} = state, time, %Resume{} = resume) when is_integer(time) do 425 - {:ok, key} = ProcQueue.enqueue(state.proc_queue, time, resume) 426 - ProcStore.track_queued(state.proc_store, resume.pid, key) 427 - 428 - state 429 - end 430 - 431 - defp queue_task(%State{} = state, time, task) when is_integer(time) do 432 - ProcQueue.enqueue(state.proc_queue, time, task) 433 - state 434 - end 435 - 436 - defp perform_next_task(%State{resumes_without_send: resumes_without_send} = state) when resumes_without_send >= 1000 do 437 - # This is a rather primitive form of deadlock detection but it works for now 438 - raise """ 439 - Resumed #{inspect(resumes_without_send)} times without sending a message! Possible deadlock? 440 - 441 - Scheduler state: 442 - 443 - #{inspect(state, pretty: true)} 444 - """ 445 - end 446 - 447 - defp perform_next_task(%State{} = state) do 448 - {time, task} = 449 - case ProcQueue.pop_next(state.proc_queue) do 450 - {:ok, {time, task}} -> 451 - {time, task} 452 - 453 - {:error, :empty} -> 454 - raise """ 455 - Attempted to call `perform_next_task/1` but the queue is empty! Possible deadlock? 456 - 457 - Scheduler state: 458 - 459 - #{inspect(state, pretty: true)} 460 - """ 461 - end 462 - 463 - if time < state.clock do 464 - raise """ 465 - Time ran backwards! 466 - 467 - Scheduler state: 468 - 469 - #{inspect(state, pretty: true)} 470 - """ 471 - end 472 - 473 - %{state | clock: time} 474 - |> perform(task) 475 - end 476 - 477 - defp perform(%State{} = state, %Spawn{} = spawn) do 478 - parent_pid = spawn.parent_resume.pid 479 - {:ok, %ProcState{node: node}} = ProcStore.fetch_state(state.proc_store, parent_pid) 480 - 481 - child_pid = do_spawn_apply(spawn.module, spawn.function, spawn.args) 482 - 483 - ProcStore.add_process(state.proc_store, child_pid, node) 484 - if spawn.link, do: ProcStore.add_link(state.proc_store, parent_pid, child_pid) 485 - 486 - log(state, {:spawn, child_pid, {spawn.module, spawn.function, spawn.args}}) 487 - 488 - parent_resume = %{%Resume{} = spawn.parent_resume | value: child_pid} 489 - 490 - state 491 - |> set_current_process(child_pid) 492 - |> queue_task(state.clock, parent_resume) 493 - end 494 - 495 - defp perform(%State{} = state, %StartNode{} = start_node) do 496 - {_name, %Node{} = node} = List.keyfind(state.nodes, start_node.name, 0) 497 - pid = do_spawn_apply(node.app_module, :start, [:temporary, node.args]) 498 - 499 - ProcStore.add_process(state.proc_store, pid, node.name) 500 - log(state, {:start_node, pid, {node.name, node.app_module, node.args}}) 501 - 502 - node = %{node | pid: pid, status: :running} 503 - state = %{state | nodes: List.keyreplace(state.nodes, node.name, 0, {node.name, node})} 504 - 505 - state 506 - |> set_current_process(pid) 507 - end 508 - 509 - defp perform(%State{} = state, %Send{} = send) do 510 - log(state, {:send, state.clock, send.dest, send.message}) 511 - 512 - state 513 - |> send_message(send) 514 - |> perform_next_task() 515 - end 516 - 517 - defp perform(%State{} = state, %Exit{} = exit) do 518 - log(state, {:exit, state.clock, exit.to_pid, exit.reason}) 519 - 520 - case ProcStore.fetch_state(state.proc_store, exit.to_pid) do 521 - {:ok, %ProcState{} = proc} -> 522 - case proc.trap_exit and exit.reason != :kill do 523 - true -> 524 - state 525 - |> send_message(%Send{dest: proc.pid, message: {:EXIT, exit.from_pid, exit.reason}}) 526 - |> perform_next_task() 527 - false -> 528 - state = set_current_process(state, proc.pid) 529 - Process.exit(proc.pid, exit.reason) 530 - 531 - state 532 - end 533 - 534 - # Process already died 535 - :error -> perform_next_task(state) 536 - end 537 - end 538 - 539 - defp perform(%State{} = state, %Resume{} = resume) do 540 - #log(state, {:resume, time, resume.pid, resume.value}) 541 - 542 - case ProcStore.fetch_state(state.proc_store, resume.pid) do 543 - {:ok, _proc} -> 544 - ProcStore.clear_queued(state.proc_store, resume.pid) 545 - 546 - state = set_current_process(state, resume.pid) 547 - send resume.pid, {:resume, resume.ref, resume.value} 548 - 549 - %{state | resumes_without_send: state.resumes_without_send + 1} 550 - 551 - :error -> 552 - # TODO: this should be unreachable because we remove queue entries proactively 553 - raise "Tried to resume dead process" 554 - end 555 - end 556 - 557 - defp perform(%State{} = state, %Timeout{pid: pid, ref: ref}) do 558 - ProcStore.clear_await(state.proc_store, pid) 559 - 560 - state = set_current_process(state, pid) 561 - send pid, {:timeout, ref} 562 - 563 - state 564 - end 565 - 566 - defp set_current_process(%State{} = state, pid) when is_pid(pid) do 567 - %{state | current: pid} 568 - end 569 - 570 - defp send_message(%State{} = state, %Send{} = send) do 571 - dest_pid = 572 - case send.dest do 573 - # TODO: for local names, send/2 actually raises if the name is not registered 574 - name when is_atom(name) -> 575 - # TODO: use node from sender 576 - case ProcRegistry.whereis(state.proc_registry, name, :nonode) do 577 - {:ok, pid} -> pid 578 - {:error, :not_found} -> nil 579 - end 580 - 581 - {name, node} when is_atom(name) and is_atom(node) -> 582 - case ProcRegistry.whereis(state.proc_registry, name, node) do 583 - {:ok, pid} -> pid 584 - {:error, :not_found} -> nil 585 - end 586 - 587 - alias when is_reference(alias) -> 588 - case ProcStore.resolve_alias(state.proc_store, alias) do 589 - {:ok, pid} -> pid 590 - :error -> nil 591 - end 592 - 593 - pid when is_pid(pid) -> pid 594 - end 595 - 596 - send_message_to_pid(state, dest_pid, send.message) 597 - end 598 - 599 - # Ignore messages sent to unregistered names 600 - # TODO: technically we should raise if name is unregistered and local 601 - # (but only for immediate send) 602 - defp send_message_to_pid(%State{} = state, nil, _message) do 603 - state 604 - end 605 - 606 - defp send_message_to_pid(%State{} = state, dest_pid, message) when is_pid(dest_pid) do 607 - send dest_pid, message 608 - state = %{state | resumes_without_send: 0} 609 - 610 - case ProcStore.fetch_state(state.proc_store, dest_pid) do 611 - {:ok, %ProcState{await: {check_fun, resume}, queue_key: timeout_key}} -> 612 - case check_fun.(message) do 613 - true -> 614 - if timeout_key, do: ProcQueue.remove(state.proc_queue, timeout_key) 615 - ProcStore.clear_await(state.proc_store, dest_pid) 616 - 617 - queue_task(state, state.clock, resume) 618 - 619 - false -> state 620 - end 621 - 622 - _ -> state 623 - end 624 - end 625 - 626 - defp clean_up_dead_process(%State{} = state, pid, reason) when is_pid(pid) do 627 - state = 628 - state 629 - |> dispatch_links(pid, reason) 630 - |> dispatch_monitors(pid, reason) 631 - 632 - :ok = remove_process(state, pid) 633 - state 634 - end 635 - 636 - # Don't send exits for reason :normal 637 - defp dispatch_links(%State{} = state, target_pid, :normal) do 638 - {:ok, %ProcState{} = target_proc} = ProcStore.fetch_state(state.proc_store, target_pid) 639 - 640 - # Clean up links for the exiting process 641 - state = Enum.reduce(target_proc.linked_to, state, fn linked_pid, state -> 642 - {:ok, %ProcState{} = linked_proc} = ProcStore.fetch_state(state.proc_store, linked_pid) 643 - 644 - state = 645 - case linked_proc.trap_exit do 646 - true -> send_message(state, %Send{dest: linked_proc.pid, message: {:EXIT, target_proc.pid, :normal}}) 647 - false -> state 648 - end 649 - 650 - ProcStore.remove_link(state.proc_store, target_pid, linked_pid) 651 - state 652 - end) 653 - 654 - state 655 - end 656 - 657 - defp dispatch_links(%State{} = state, target_pid, reason) when is_pid(target_pid) do 658 - {:ok, %ProcState{} = target_proc} = ProcStore.fetch_state(state.proc_store, target_pid) 659 - 660 - {state, pids_to_kill} = traverse_linked(state, target_proc, reason) 661 - # Cycles are prevented, but there could still be duplicates 662 - pids_to_kill = Enum.uniq(pids_to_kill) 663 - 664 - # Kill processes 665 - Enum.each(pids_to_kill, fn pid -> 666 - kill_process(pid, reason) 667 - end) 668 - 669 - # Dispatch monitors for all killed processes 670 - state = 671 - Enum.reduce(pids_to_kill, state, fn pid, state -> 672 - dispatch_monitors(state, pid, reason) 673 - end) 674 - 675 - # Once processes are all dead and monitors dispatched, 676 - # we can remove them from the queue and store 677 - # 678 - # Note that these processes still have links to each other, 679 - # but it does not matter because both sides of the links will be deleted 680 - # along with them 681 - Enum.each(pids_to_kill, fn pid -> 682 - remove_process(state, pid) 683 - end) 684 - 685 - state 686 - end 687 - 688 - defp traverse_linked(%State{} = state, %ProcState{} = target_proc, reason, parents \\ MapSet.new()) do 689 - target_proc.linked_to 690 - |> Enum.reject(&MapSet.member?(parents, &1)) 691 - |> Enum.reduce({state, []}, fn linked_pid, {state, acc} -> 692 - {:ok, %ProcState{} = linked_proc} = ProcStore.fetch_state(state.proc_store, linked_pid) 693 - 694 - case linked_proc.trap_exit do 695 - true -> 696 - # Since one side of this link will survive, we must clean up the link 697 - ProcStore.remove_link(state.proc_store, target_proc.pid, linked_pid) 698 - 699 - # Send trapped EXIT message 700 - message = {:EXIT, target_proc.pid, reason} 701 - state = send_message(state, %Send{dest: linked_pid, message: message}) 702 - 703 - {state, acc} 704 - 705 - false -> 706 - {state, to_kill} = traverse_linked(state, linked_proc, reason, MapSet.put(parents, target_proc.pid)) 707 - {state, acc ++ [linked_proc.pid | to_kill]} 708 - end 709 - end) 710 - end 711 - 712 - defp dispatch_monitors(%State{} = state, target_pid, reason) when is_pid(target_pid) do 713 - {:ok, %ProcState{} = target_proc} = ProcStore.fetch_state(state.proc_store, target_pid) 714 - 715 - target_proc.monitored_by 716 - |> Enum.reduce(state, fn {ref, to_pid}, state -> 717 - ProcStore.remove_monitor(state.proc_store, to_pid, ref) 718 - 719 - msg = {:DOWN, ref, :process, target_pid, reason} 720 - send_message(state, %Send{dest: to_pid, message: msg}) 721 - end) 722 - end 723 - 724 - defp log(%State{} = state, event) do 725 - SimLog.log(state.log_server_pid, event) 726 - end 727 - 728 - @spec do_spawn_apply(module, atom, list) :: pid 729 - defp do_spawn_apply(m, f, a) do 730 - scheduler_pid = self() 731 - seed = random_seed() 732 - spawn_link(fn -> 733 - Hobbes.Construct.SimServer.set_scheduler_pid(scheduler_pid) 734 - :rand.seed(:exsss, seed) 735 - apply(m, f, a) 736 - end) 737 - end 738 - 739 - defp kill_node(%State{} = state, %Node{} = node) do 740 - ProcStore.list_processes(state.proc_store) 741 - |> Enum.filter(&(&1.node == node.name)) 742 - |> Enum.each(fn %ProcState{pid: pid} -> 743 - # TODO: the order here is not deterministic, but it probably doesn't matter? 744 - :ok = kill_process(pid, :shutdown) 745 - :ok = remove_process(state, pid) 746 - end) 747 - :ok 748 - end 749 - 750 - # Note: remember to call remove_process() afterwards to clear the process from the ProcStore 751 - defp kill_process(pid, reason) do 752 - Process.exit(pid, reason) 753 - receive do 754 - {:EXIT, ^pid, ^reason} -> :noop 755 - after 756 - # Sanity to stop the scheduler hanging, this should never happen 757 - 1000 -> raise "Process #{inspect(pid)} failed to exit" 758 - end 759 - :ok 760 - end 761 - 762 - defp remove_process(%State{} = state, pid) when is_pid(pid) do 763 - {:ok, %ProcState{queue_key: queue_key}} = ProcStore.fetch_state(state.proc_store, pid) 764 - if queue_key, do: ProcQueue.remove(state.proc_queue, queue_key) 765 - 766 - ProcStore.remove_process(state.proc_store, pid) 767 - ProcRegistry.remove_process(state.proc_registry, pid) 768 - :ok 769 - end 770 - 771 - defp random_seed, do: Enum.random(1..1_000_000) 772 - end
-126
lib/construct/scheduler/file_store.ex
··· 1 - defmodule Hobbes.Construct.Scheduler.FileStore do 2 - alias Hobbes.KV.FlatKV 3 - 4 - @type t :: FlatKV.t 5 - @type posix :: :enoent | :eexist | :enotdir | :eisdir 6 - 7 - @spec new :: t 8 - def new do 9 - kv = FlatKV.new(public: true) 10 - FlatKV.put(kv, "/", "directory") 11 - 12 - kv 13 - end 14 - 15 - @spec mkdir(t, binary) :: :ok | {:error, posix} 16 - def mkdir(kv, path) do 17 - path = normalize_directory(path) 18 - 19 - case all_components_dir?(kv, path) do 20 - true -> 21 - case exists?(kv, path) do 22 - false -> 23 - FlatKV.put(kv, path, "directory") 24 - :ok 25 - 26 - true -> {:error, :eexist} 27 - end 28 - 29 - false -> {:error, :enoent} 30 - end 31 - end 32 - 33 - @spec mkdir_p(t, binary) :: :ok | {:error, posix} 34 - def mkdir_p(kv, path) do 35 - path 36 - |> components() 37 - |> Enum.reduce_while(:ok, fn p, :ok -> 38 - case exists?(kv, p) do 39 - false -> 40 - :ok = mkdir(kv, p) 41 - {:cont, :ok} 42 - 43 - true -> 44 - case dir?(kv, p) do 45 - true -> {:cont, :ok} 46 - false -> {:halt, {:error, :enotdir}} 47 - end 48 - end 49 - end) 50 - end 51 - 52 - @spec write(t, binary, binary) :: :ok | {:error, posix} 53 - def write(kv, path, contents) do 54 - case all_components_dir?(kv, path) do 55 - true -> 56 - FlatKV.put(kv, path, "file:" <> contents) 57 - :ok 58 - false -> 59 - {:error, :enoent} 60 - end 61 - end 62 - 63 - @spec read(t, binary) :: {:ok, binary} | {:error, posix} 64 - def read(kv, path) do 65 - case FlatKV.get(kv, path) do 66 - "file:" <> contents -> 67 - {:ok, contents} 68 - 69 - "directory" -> 70 - {:error, :eisdir} 71 - 72 - nil -> 73 - {:error, :enoent} 74 - end 75 - end 76 - 77 - @spec ls(t, binary) :: {:ok, [binary]} 78 - def ls(kv, path) do 79 - # TODO: error handling (enoent, enotdir) 80 - path = normalize_directory(path) 81 - # TODO: would be more efficient to only scan keys 82 - %{pairs: pairs} = FlatKV.scan(kv, path <> "/", path <> "0") 83 - 84 - child_paths = 85 - pairs 86 - |> Enum.map(fn {^path <> "/" <> file_name, _contents} -> file_name end) 87 - |> Enum.reject(fn name -> String.contains?(name, "/") end) 88 - 89 - {:ok, child_paths} 90 - end 91 - 92 - @spec dir?(t, binary) :: boolean 93 - def dir?(kv, path) do 94 - path = normalize_directory(path) 95 - FlatKV.get(kv, path) == "directory" 96 - end 97 - 98 - @spec exists?(t, binary) :: boolean 99 - def exists?(kv, path) do 100 - FlatKV.get(kv, path) != nil 101 - end 102 - 103 - defp components(path) do 104 - split = Path.split(path) 105 - 106 - Enum.map(1..length(split), fn count -> 107 - Enum.take(split, count) |> Path.join() 108 - end) 109 - end 110 - 111 - defp all_components_dir?(kv, path) do 112 - path 113 - |> components() 114 - |> case do 115 - [] -> [] 116 - list -> List.delete_at(list, -1) 117 - end 118 - |> Enum.all?(&dir?(kv, &1)) 119 - end 120 - 121 - defp normalize_directory("/"), do: "/" 122 - defp normalize_directory(path), do: String.trim_trailing(path, "/") 123 - 124 - @doc false 125 - def dump(kv), do: FlatKV.dump(kv) 126 - end
-45
lib/construct/scheduler/proc_queue.ex
··· 1 - defmodule Hobbes.Construct.Scheduler.ProcQueue do 2 - @type t :: :ets.table 3 - @type key :: {non_neg_integer, non_neg_integer} 4 - 5 - def new do 6 - :ets.new(:proc_queue, [:private, :ordered_set]) 7 - end 8 - 9 - @spec enqueue(t, non_neg_integer, term) :: {:ok, key} 10 - def enqueue(table, time, task) when is_integer(time) do 11 - # Tasks are enqueued with key {time, i} to ensure FIFO order for a given time 12 - i = 13 - case :ets.prev(table, {time, :infinity}) do 14 - {^time, i} -> i + 1 15 - _ -> 0 16 - end 17 - 18 - key = {time, i} 19 - :ets.insert(table, {key, task}) 20 - {:ok, key} 21 - end 22 - 23 - @spec remove(t, key) :: :ok 24 - def remove(table, key) do 25 - case :ets.member(table, key) do 26 - true -> 27 - :ets.delete(table, key) 28 - :ok 29 - false -> 30 - raise "Tried to remove key that does not exist: #{inspect(key)}" 31 - end 32 - end 33 - 34 - @spec pop_next(t) :: {:ok, {non_neg_integer, term}} | {:error, :empty} 35 - def pop_next(table) do 36 - case :ets.first_lookup(table) do 37 - {_key, [{{time, _i} = key, task}]} -> 38 - :ets.delete(table, key) 39 - {:ok, {time, task}} 40 - 41 - :"$end_of_table" -> 42 - {:error, :empty} 43 - end 44 - end 45 - end
-58
lib/construct/scheduler/proc_registry.ex
··· 1 - defmodule Hobbes.Construct.Scheduler.ProcRegistry do 2 - @type t :: :ets.table 3 - 4 - @spec new :: t 5 - def new do 6 - :ets.new(:proc_registry, [:set, :private]) 7 - end 8 - 9 - @spec register(t, pid, atom, atom) :: :ok | {:error, :name_already_registered | :pid_already_registered} 10 - def register(table, pid, name, node) when is_pid(pid) and is_atom(name) and is_atom(node) do 11 - case whereis(table, name, node) do 12 - {:error, :not_found} -> 13 - case :ets.lookup(table, pid) do 14 - [] -> 15 - :ets.insert(table, {{name, node}, pid}) 16 - :ets.insert(table, {pid, {name, node}}) 17 - :ok 18 - 19 - [{^pid, _value}] -> {:error, :pid_already_registered} 20 - end 21 - 22 - {:ok, _pid} -> {:error, :name_already_registered} 23 - end 24 - end 25 - 26 - @spec whereis(t, atom, atom) :: {:ok, pid} | {:error, :not_found} 27 - def whereis(table, name, node) when is_atom(name) and is_atom(node) do 28 - case :ets.lookup(table, {name, node}) do 29 - [{{^name, ^node}, pid}] -> {:ok, pid} 30 - [] -> {:error, :not_found} 31 - end 32 - end 33 - 34 - @spec unregister(t, atom, atom) :: :ok | :error 35 - def unregister(table, name, node) when is_atom(name) and is_atom(node) do 36 - case whereis(table, name, node) do 37 - {:ok, pid} -> 38 - :ets.delete(table, {name, node}) 39 - :ets.delete(table, pid) 40 - :ok 41 - 42 - {:error, :not_found} -> 43 - :error 44 - end 45 - end 46 - 47 - @spec remove_process(t, pid) :: :ok | :error 48 - def remove_process(table, pid) when is_pid(pid) do 49 - case :ets.lookup(table, pid) do 50 - [{pid, {name, node}}] -> 51 - :ets.delete(table, {name, node}) 52 - :ets.delete(table, pid) 53 - :ok 54 - 55 - [] -> :error 56 - end 57 - end 58 - end
-220
lib/construct/scheduler/proc_store.ex
··· 1 - defmodule Hobbes.Construct.Scheduler.ProcStore do 2 - alias Hobbes.Construct.Scheduler.{ProcStore, ProcQueue} 3 - 4 - @type check_fun :: (-> boolean) 5 - 6 - defmodule ProcState do 7 - @type t :: %__MODULE__{ 8 - pid: pid, 9 - node: atom, 10 - monitors: %{reference => pid}, 11 - monitored_by: [{reference, pid}], 12 - linked_to: [pid], 13 - trap_exit: boolean, 14 - queue_key: ProcQueue.key | nil, 15 - await: {ProcStore.check_fun, term} | nil, 16 - } 17 - @enforce_keys [:pid, :node] 18 - defstruct [ 19 - monitors: %{}, 20 - monitored_by: [], 21 - linked_to: [], 22 - trap_exit: false, 23 - queue_key: nil, 24 - await: nil, 25 - ] ++ @enforce_keys 26 - end 27 - 28 - @type t :: %__MODULE__{ 29 - proc_table: :ets.table, 30 - alias_table: :ets.table, 31 - } 32 - 33 - @enforce_keys [:proc_table, :alias_table] 34 - defstruct @enforce_keys 35 - 36 - def new do 37 - %ProcStore{ 38 - proc_table: :ets.new(:proc_table, [:set, :private]), 39 - alias_table: :ets.new(:alias_table, [:set, :private]), 40 - } 41 - end 42 - 43 - @spec add_process(t, pid, atom) :: :ok 44 - def add_process(%ProcStore{} = ps, pid, node) when is_pid(pid) and is_atom(node) do 45 - state = %ProcState{pid: pid, node: node} 46 - 47 - :ets.insert(ps.proc_table, {pid, state}) 48 - :ok 49 - end 50 - 51 - @spec remove_process(t, pid) :: :ok 52 - def remove_process(%ProcStore{} = ps, pid) when is_pid(pid) do 53 - case fetch_state(ps, pid) do 54 - {:ok, _proc} -> 55 - :ets.delete(ps.proc_table, pid) 56 - :ok 57 - :error -> 58 - raise "Process #{inspect(pid)} cannot be removed because it does not exist" 59 - end 60 - end 61 - 62 - @spec fetch_state(t, pid) :: {:ok, ProcState.t} | :error 63 - def fetch_state(%ProcStore{} = ps, pid) when is_pid(pid) do 64 - case :ets.lookup(ps.proc_table, pid) do 65 - [{^pid, %ProcState{} = state}] -> 66 - {:ok, state} 67 - _ -> 68 - :error 69 - end 70 - end 71 - 72 - @spec track_queued(t, pid, ProcQueue.key) :: :ok 73 - def track_queued(%ProcStore{} = ps, pid, {_, _} = queue_key) when is_pid(pid) do 74 - {:ok, %ProcState{} = state} = fetch_state(ps, pid) 75 - if state.queue_key != nil, do: raise "Process is already in queue: #{inspect(state)}" 76 - 77 - state = %{state | queue_key: queue_key} 78 - :ets.insert(ps.proc_table, {pid, state}) 79 - :ok 80 - end 81 - 82 - @spec clear_queued(t, pid) :: :ok 83 - def clear_queued(%ProcStore{} = ps, pid) do 84 - {:ok, %ProcState{} = state} = fetch_state(ps, pid) 85 - 86 - :ets.insert(ps.proc_table, {pid, %{state | queue_key: nil}}) 87 - :ok 88 - end 89 - 90 - @spec track_await(t, pid, check_fun, term, ProcQueue.key | nil) :: :ok 91 - def track_await(%ProcStore{} = ps, pid, check_fun, resume, timeout_queue_key) 92 - when is_pid(pid) and is_function(check_fun) and is_struct(resume) and (is_nil(timeout_queue_key) or is_tuple(timeout_queue_key)) do 93 - {:ok, %ProcState{} = state} = fetch_state(ps, pid) 94 - if state.queue_key != nil, do: raise "Process is in queue: #{inspect(state)}" 95 - 96 - state = %{state | await: {check_fun, resume}, queue_key: timeout_queue_key} 97 - :ets.insert(ps.proc_table, {pid, state}) 98 - :ok 99 - end 100 - 101 - @spec clear_await(t, pid) :: :ok 102 - def clear_await(%ProcStore{} = ps, pid) do 103 - {:ok, %ProcState{} = state} = fetch_state(ps, pid) 104 - if !state.await, do: raise "Process is not awaiting: #{inspect(state)}" 105 - 106 - state = %{state | await: nil, queue_key: nil} 107 - :ets.insert(ps.proc_table, {pid, state}) 108 - :ok 109 - end 110 - 111 - @spec add_link(t, pid, pid) :: :ok | :error 112 - def add_link(%ProcStore{} = ps, pid1, pid2) do 113 - {:ok, %ProcState{} = state1} = fetch_state(ps, pid1) 114 - {:ok, %ProcState{} = state2} = fetch_state(ps, pid2) 115 - 116 - case pid2 in state1.linked_to do 117 - false -> 118 - state1 = Map.update!(state1, :linked_to, &[pid2 | &1]) 119 - state2 = Map.update!(state2, :linked_to, &[pid1 | &1]) 120 - 121 - :ets.insert(ps.proc_table, {pid1, state1}) 122 - :ets.insert(ps.proc_table, {pid2, state2}) 123 - :ok 124 - 125 - true -> :error 126 - end 127 - end 128 - 129 - @spec remove_link(t, pid, pid) :: :ok | :error 130 - def remove_link(%ProcStore{} = ps, pid1, pid2) do 131 - {:ok, %ProcState{} = state1} = fetch_state(ps, pid1) 132 - {:ok, %ProcState{} = state2} = fetch_state(ps, pid2) 133 - 134 - case pid2 in state1.linked_to do 135 - true -> 136 - state1 = Map.update!(state1, :linked_to, &List.delete(&1, pid2)) 137 - state2 = Map.update!(state2, :linked_to, &List.delete(&1, pid1)) 138 - 139 - :ets.insert(ps.proc_table, {pid1, state1}) 140 - :ets.insert(ps.proc_table, {pid2, state2}) 141 - :ok 142 - 143 - false -> :error 144 - end 145 - end 146 - 147 - @spec add_monitor(t, pid, pid, reference) :: :ok 148 - def add_monitor(%ProcStore{} = ps, pid, target_pid, ref) when is_pid(pid) and is_pid(target_pid) and is_reference(ref) do 149 - {:ok, %ProcState{} = proc_state} = fetch_state(ps, pid) 150 - {:ok, %ProcState{} = target_state} = fetch_state(ps, target_pid) 151 - 152 - monitors = Map.put(proc_state.monitors, ref, target_pid) 153 - monitored_by = [{ref, pid} | target_state.monitored_by] 154 - 155 - :ets.insert(ps.proc_table, {pid, %{proc_state | monitors: monitors}}) 156 - :ets.insert(ps.proc_table, {target_pid, %{target_state | monitored_by: monitored_by}}) 157 - :ok 158 - end 159 - 160 - @spec remove_monitor(t, pid, reference) :: :ok 161 - def remove_monitor(%ProcStore{} = ps, pid, ref) do 162 - {:ok, %ProcState{} = proc_state} = fetch_state(ps, pid) 163 - 164 - {target_pid, monitors} = Map.pop!(proc_state.monitors, ref) 165 - 166 - {:ok, %ProcState{} = target_state} = fetch_state(ps, target_pid) 167 - monitored_by = 168 - target_state.monitored_by 169 - |> Enum.filter(fn 170 - {^ref, _pid} -> false 171 - _ -> true 172 - end) 173 - 174 - :ets.insert(ps.proc_table, {pid, %{proc_state | monitors: monitors}}) 175 - :ets.insert(ps.proc_table, {target_pid, %{target_state | monitored_by: monitored_by}}) 176 - :ok 177 - end 178 - 179 - @spec add_alias(t, pid, reference) :: :ok 180 - def add_alias(%ProcStore{} = ps, pid, alias) when is_pid(pid) and is_reference(alias) do 181 - :ets.insert(ps.alias_table, {alias, pid}) 182 - :ok 183 - end 184 - 185 - @spec remove_alias(t, pid, reference) :: :ok | {:error, :wrong_process | :not_found} 186 - def remove_alias(%ProcStore{} = ps, pid, alias) when is_pid(pid) and is_reference(alias) do 187 - case resolve_alias(ps, alias) do 188 - {:ok, ^pid} -> 189 - :ets.delete(ps.alias_table, alias) 190 - :ok 191 - 192 - {:ok, _other} -> {:error, :wrong_process} 193 - :error -> {:error, :not_found} 194 - end 195 - end 196 - 197 - @spec resolve_alias(t, reference) :: {:ok, pid} | :error 198 - def resolve_alias(%ProcStore{} = ps, alias) when is_reference(alias) do 199 - case :ets.lookup(ps.alias_table, alias) do 200 - [{^alias, pid}] when is_pid(pid) -> 201 - {:ok, pid} 202 - [] -> 203 - :error 204 - end 205 - end 206 - 207 - @spec set_flag(t, pid, :trap_exit, boolean) :: {:ok, boolean} 208 - def set_flag(%ProcStore{} = ps, pid, :trap_exit, value) when is_boolean(value) do 209 - {:ok, %ProcState{} = state} = fetch_state(ps, pid) 210 - 211 - :ets.insert(ps.proc_table, {pid, %{state | trap_exit: value}}) 212 - {:ok, state.trap_exit} 213 - end 214 - 215 - @spec list_processes(t) :: [ProcState.t] 216 - def list_processes(%ProcStore{} = ps) do 217 - :ets.tab2list(ps.proc_table) 218 - |> Enum.map(fn {_pid, %ProcState{} = proc} -> proc end) 219 - end 220 - end
-74
lib/construct/sim_file.ex
··· 1 - defmodule Hobbes.Construct.SimFile do 2 - alias Hobbes.Construct.Scheduler 3 - alias Hobbes.Construct.Scheduler.FileStore 4 - 5 - import Hobbes.Construct.SimServer, only: [get_scheduler_pid: 0] 6 - 7 - @spec mkdir(binary) :: :ok | {:error, File.posix} 8 - def mkdir(path) when is_binary(path) do 9 - case get_scheduler_pid() do 10 - spid when is_pid(spid) -> 11 - fs = Scheduler.get_file_store(spid) 12 - FileStore.mkdir(fs, path) 13 - end 14 - end 15 - 16 - @spec mkdir_p(binary) :: :ok | {:error, File.posix} 17 - def mkdir_p(path) when is_binary(path) do 18 - case get_scheduler_pid() do 19 - spid when is_pid(spid) -> 20 - fs = Scheduler.get_file_store(spid) 21 - FileStore.mkdir_p(fs, path) 22 - end 23 - end 24 - 25 - @spec write(binary, binary) :: :ok | {:error, File.posix} 26 - def write(path, contents) when is_binary(path) and is_binary(contents) do 27 - case get_scheduler_pid() do 28 - spid when is_pid(spid) -> 29 - fs = Scheduler.get_file_store(spid) 30 - FileStore.write(fs, path, contents) 31 - end 32 - end 33 - 34 - @spec read(binary) :: {:ok, binary} | {:error, File.posix} 35 - def read(path) when is_binary(path) do 36 - case get_scheduler_pid() do 37 - spid when is_pid(spid) -> 38 - fs = Scheduler.get_file_store(spid) 39 - FileStore.read(fs, path) 40 - end 41 - end 42 - 43 - @spec ls(binary) :: {:ok, [binary]} 44 - def ls(path) when is_binary(path) do 45 - case get_scheduler_pid() do 46 - spid when is_pid(spid) -> 47 - fs = Scheduler.get_file_store(spid) 48 - FileStore.ls(fs, path) 49 - end 50 - end 51 - 52 - @spec exists?(binary) :: boolean 53 - def exists?(path) when is_binary(path) do 54 - case get_scheduler_pid() do 55 - spid when is_pid(spid) -> 56 - fs = Scheduler.get_file_store(spid) 57 - FileStore.exists?(fs, path) 58 - end 59 - end 60 - 61 - @doc false 62 - def dump_paths do 63 - case get_scheduler_pid() do 64 - spid when is_pid(spid) -> 65 - fs = Scheduler.get_file_store(spid) 66 - 67 - FileStore.dump(fs) 68 - |> Enum.map(fn {path, _contents} -> path end) 69 - 70 - nil -> 71 - raise "SimFile.dump_paths/0 is a debug function which can only be called in simulation" 72 - end 73 - end 74 - end
-106
lib/construct/sim_internal.ex
··· 1 - defmodule Hobbes.Construct.SimInternal do 2 - @moduledoc """ 3 - Internal functions for running a `SimServer` (registration, receive loop, etc). 4 - 5 - Similar to erlang's `:gen` but for `SimServer`. 6 - """ 7 - 8 - require Logger 9 - alias Hobbes.Construct.{SimServer, Scheduler} 10 - import Hobbes.Construct.SimServer, only: [yield_receive: 2, yield_receive: 3, fetch_scheduler_pid!: 0] 11 - 12 - @type name :: {:global, term} 13 - 14 - @spec start(module, term, Keyword.t) :: {:ok, pid} 15 - def start(module, init_arg, opts) do 16 - scheduler_pid = fetch_scheduler_pid!() 17 - pid = Scheduler.spawn_and_yield(scheduler_pid, :link, __MODULE__, :server_init, [module, init_arg, opts, self()]) 18 - 19 - # We await ack from the spawned SimServer just like :proc_lib.start() 20 - # Timeout is for sanity, an init should never take so long 21 - yield_receive(scheduler_pid, 60_000) do 22 - {:ack, ^pid} -> 23 - if name = opts[:name], do: SimServer.register(pid, name) 24 - {:ok, pid} 25 - 26 - # TODO: error handling? 27 - end 28 - end 29 - 30 - # These functions run within the SimServer process 31 - 32 - def server_init(module, arg, _options, parent) 33 - when is_atom(module) and is_pid(parent) do 34 - {:ok, initial_state} = module.init(arg) 35 - 36 - SimServer.send parent, {:ack, self()} 37 - sim_loop(module, initial_state) 38 - end 39 - 40 - defp sim_loop(module, state) do 41 - state = yield_receive(fetch_scheduler_pid!()) do 42 - message -> dispatch(message, module, state) 43 - end 44 - 45 - sim_loop(module, state) 46 - end 47 - 48 - defp dispatch({:"$sim_call", {_pid, [:alias | alias_ref] = tag} = from, request}, module, state) do 49 - try do 50 - module.handle_call(request, from, state) 51 - catch 52 - :exit, reason -> 53 - log_exit(reason, __STACKTRACE__, state) 54 - exit(reason) 55 - end 56 - |> case do 57 - {:reply, response, state} -> 58 - SimServer.send alias_ref, {tag, response} 59 - state 60 - {:noreply, state} -> 61 - state 62 - # TODO: error message for invalid reply 63 - end 64 - end 65 - 66 - defp dispatch({:"$sim_cast", request}, module, state) do 67 - try do 68 - module.handle_cast(request, state) 69 - catch 70 - :exit, reason -> 71 - log_exit(reason, __STACKTRACE__, state) 72 - exit(reason) 73 - end 74 - |> case do 75 - {:noreply, state} -> state 76 - # TODO: error message for invalid reply 77 - end 78 - end 79 - 80 - defp dispatch(message, module, state) do 81 - try do 82 - module.handle_info(message, state) 83 - catch 84 - :exit, reason -> 85 - log_exit(reason, __STACKTRACE__, state) 86 - exit(reason) 87 - end 88 - |> case do 89 - {:noreply, state} -> state 90 - # TODO: error message for invalid reply 91 - end 92 - end 93 - 94 - defp log_exit(:normal, _stacktrace, _state), do: :noop 95 - defp log_exit(:shutdown, _stacktrace, _state), do: :noop 96 - 97 - defp log_exit(reason, stacktrace, state) do 98 - require Logger 99 - Logger.error """ 100 - SimServer #{inspect(self())} terminating 101 - ** (stop) #{Exception.format_exit(reason)} 102 - #{Exception.format_stacktrace(stacktrace)}\ 103 - State: #{inspect(state)}\ 104 - """ 105 - end 106 - end
-124
lib/construct/sim_log.ex
··· 1 - defmodule Hobbes.Construct.SimLog do 2 - use GenServer 3 - 4 - defmodule Known do 5 - @enforce_keys [:pids, :refs] 6 - defstruct @enforce_keys 7 - end 8 - 9 - defmodule State do 10 - @type t :: %__MODULE__{ 11 - num_events: non_neg_integer, 12 - rolling_hash: term, 13 - known: %Known{}, 14 - log: list, 15 - } 16 - @enforce_keys [:known, :log, :rolling_hash, :num_events] 17 - defstruct @enforce_keys 18 - end 19 - 20 - def start_link(args) do 21 - GenServer.start_link(__MODULE__, args) 22 - end 23 - 24 - @spec get_log(term) :: list 25 - def get_log(server) do 26 - GenServer.call(server, :get_log) 27 - end 28 - 29 - @spec log(term, term) :: :ok 30 - def log(server, event) do 31 - GenServer.cast(server, {:log, event}) 32 - end 33 - 34 - def init(_) do 35 - {:ok, %State{ 36 - num_events: 0, 37 - rolling_hash: nil, 38 - known: %Known{pids: %{}, refs: %{}}, 39 - log: [], 40 - }} 41 - end 42 - 43 - def handle_call(:get_log, _from, %State{} = state) do 44 - response = %{ 45 - num_events: state.num_events, 46 - rolling_hash: state.rolling_hash, 47 - known: state.known, 48 - log: Enum.reverse(state.log), 49 - } 50 - {:reply, response, state} 51 - end 52 - 53 - def handle_cast({:log, event}, %State{} = state) do 54 - {known, event} = homogenize(state.known, event) 55 - 56 - state = %{state | 57 - num_events: state.num_events + 1, 58 - rolling_hash: :erlang.phash2({state.rolling_hash, event}), 59 - known: known, 60 - # TODO: configurable 61 - #log: [event | state.log], 62 - } 63 - 64 - {:noreply, state} 65 - end 66 - 67 - defmodule Homogenized do 68 - @enforce_keys [:type, :i] 69 - defstruct @enforce_keys 70 - end 71 - 72 - defimpl Inspect, for: Homogenized do 73 - def inspect(%Homogenized{type: :pid, i: i}, _opts), do: "#sPID<#{i}>" 74 - def inspect(%Homogenized{type: :ref, i: i}, _opts), do: "#sRef<#{i}>" 75 - end 76 - 77 - defp homogenize(%Known{} = known, value) when is_pid(value) do 78 - case Map.get(known.pids, value) do 79 - nil -> 80 - hmg = %Homogenized{type: :pid, i: map_size(known.pids)} 81 - known = %Known{known | pids: Map.put(known.pids, value, hmg)} 82 - {known, Map.fetch!(known.pids, value)} 83 - %Homogenized{} = hmg -> 84 - {known, hmg} 85 - end 86 - end 87 - 88 - defp homogenize(%Known{} = known, value) when is_reference(value) do 89 - case Map.get(known.refs, value) do 90 - nil -> 91 - hmg = %Homogenized{type: :ref, i: map_size(known.refs)} 92 - known = %Known{known | refs: Map.put(known.refs, value, hmg)} 93 - {known, Map.fetch!(known.refs, value)} 94 - %Homogenized{} = hmg -> 95 - {known, hmg} 96 - end 97 - end 98 - 99 - defp homogenize(%Known{} = known, [:alias | ref]) when is_reference(ref) do 100 - {known, h_ref} = homogenize(known, ref) 101 - {known, [:alias | h_ref]} 102 - end 103 - 104 - defp homogenize(%Known{} = known, list) when is_list(list) do 105 - {known, h_list} = Enum.reduce(list, {known, []}, fn element, {known, acc_list} -> 106 - {k, h_el} = homogenize(known, element) 107 - {k, [h_el | acc_list]} 108 - end) 109 - {known, Enum.reverse(h_list)} 110 - end 111 - 112 - defp homogenize(%Known{} = known, tuple) when is_tuple(tuple) do 113 - {known, h_list} = homogenize(known, Tuple.to_list(tuple)) 114 - {known, List.to_tuple(h_list)} 115 - end 116 - 117 - # This one also handles structs 118 - defp homogenize(%Known{} = known, map) when is_map(map) do 119 - {known, h_list} = homogenize(known, Map.to_list(map)) 120 - {known, Map.new(h_list)} 121 - end 122 - 123 - defp homogenize(%Known{} = known, value), do: {known, value} 124 - end
-458
lib/construct/sim_server.ex
··· 1 - defmodule Hobbes.Construct.SimServer do 2 - alias Hobbes.Construct.{SimInternal, Scheduler, SimLog} 3 - 4 - # TODO: support more GenServer init/1 return types here 5 - @callback init(init_arg :: term) :: {:ok, term} 6 - 7 - defmacro __using__(_opts) do 8 - quote do 9 - alias Hobbes.Construct.SimServer 10 - use GenServer 11 - end 12 - end 13 - 14 - @doc """ 15 - Yields until a matching message is received. 16 - 17 - ## Examples 18 - 19 - yield_receive(get_scheduler_pid()) do 20 - :good_message -> :ok 21 - :bad_message -> :error 22 - end 23 - 24 - """ 25 - defmacro yield_receive(scheduler_pid, timeout \\ :infinity, [do: do_block]) do 26 - # Here, we use `do_block` to build a function which 27 - # checks if a value matches the intended receive block 28 - # 29 - # e.g. 30 - # 31 - # yield_receive do 32 - # {:ok, :hello} -> :ok 33 - # {:error, error} -> error 34 - # end 35 - # 36 - # becomes... 37 - # 38 - # fn 39 - # {:ok, :hello} -> true 40 - # {:error, error} -> true 41 - # _ -> false 42 - # end 43 - check_block = 44 - Enum.map(do_block, fn {:->, _meta, [clause, _result]} -> 45 - {:->, [], [clause, true]} 46 - end) 47 - final_clause = 48 - quote do 49 - _ -> false 50 - end 51 - check_block = check_block ++ final_clause 52 - check_func_ast = {:fn, [], check_block} 53 - # Walk the ast and mark as generated to prevent warnings about unused variables 54 - # and redundant clauses 55 - # (also remove line numbers just because) 56 - check_func_ast = Macro.prewalk(check_func_ast, fn 57 - {a, meta, b} -> {a, meta |> Keyword.drop([:line, :column]) |> Keyword.put(:generated, true), b} 58 - other -> other 59 - end) 60 - 61 - quote do 62 - check_func = unquote(check_func_ast) 63 - 64 - # We use a ref to communicate whether a value was actually received 65 - # (since it is impossible for receive to return our unique ref) 66 - ref = make_ref() 67 - receive_func = fn -> 68 - receive do 69 - unquote(do_block) 70 - after 71 - 0 -> ref 72 - end 73 - end 74 - 75 - Hobbes.Construct.Scheduler.yield(unquote(scheduler_pid)) 76 - # We first check if the message queue already has a matching message 77 - # If so, we don't want to yield_until_message because the Scheduler would 78 - # have no way of knowing that a matching message is already in our queue 79 - case receive_func.() do 80 - ^ref -> 81 - # If there is no matching message, we yield until one is sent to us 82 - Hobbes.Construct.Scheduler.yield_until_message(unquote(scheduler_pid), check_func, unquote(timeout)) 83 - 84 - # Now that a message has arrived, we extract it with the receive 85 - # The case is a sanity check 86 - case receive_func.() do 87 - ^ref -> raise "Resumed for message but no message received!" 88 - value -> value 89 - end 90 - 91 - # A matching message was already in our queue, so we simply return it 92 - value -> 93 - value 94 - end 95 - end 96 - end 97 - 98 - @type request_id :: :gen_server.request_id | reference 99 - 100 - @scheduler_key :scheduler_key 101 - def set_scheduler_pid(pid), do: nil = Process.put(@scheduler_key, pid) 102 - def get_scheduler_pid, do: Process.get(@scheduler_key) 103 - def fetch_scheduler_pid!, do: get_scheduler_pid() || raise "No scheduler pid!" 104 - 105 - @spec simulated? :: boolean 106 - def simulated?, do: get_scheduler_pid() != nil 107 - 108 - @spec start_scheduler() :: {:ok, pid} 109 - def start_scheduler(seed \\ 100) do 110 - {:ok, scheduler_pid} = Scheduler.start_link(seed) 111 - 112 - {:ok, log_pid} = SimLog.start_link(nil) 113 - Scheduler.configure_log_server(scheduler_pid, log_pid) 114 - 115 - {:ok, scheduler_pid} 116 - end 117 - 118 - @doc """ 119 - Starts a `SimServer`. 120 - 121 - ## Examples 122 - 123 - iex> SimServer.start(__MODULE__, nil, name: {:global, :my_server}) 124 - {:ok, #PID{0.0.0}} 125 - 126 - """ 127 - @spec start_link(module, term, Keyword.t) :: GenServer.on_start 128 - def start_link(module, init_arg, options \\ []) do 129 - case get_scheduler_pid() do 130 - nil -> 131 - GenServer.start_link(module, init_arg, options) 132 - scheduler_pid when is_pid(scheduler_pid) -> 133 - SimInternal.start(module, init_arg, options) 134 - end 135 - end 136 - 137 - @doc """ 138 - Performs a `SimServer` call. 139 - 140 - ## Examples 141 - 142 - iex> SimServer.call({:global, :my_server}, :foo) 143 - :bar 144 - 145 - """ 146 - @spec call(term, term, timeout) :: term 147 - def call(server, request, timeout \\ 5000) do 148 - case get_scheduler_pid() do 149 - nil -> 150 - GenServer.call(server, request, timeout) 151 - scheduler_pid when is_pid(scheduler_pid) -> 152 - sim_call(scheduler_pid, server, request, timeout) 153 - end 154 - end 155 - 156 - @doc """ 157 - Sends a request to a SimServer. 158 - 159 - See `:gen_server.send_request/2` and `SimServer.receive_response/2`. 160 - 161 - ## Examples 162 - 163 - iex> req_id = SimServer.send_request(pid, :foo) 164 - iex> SimServer.receive_response(req_id) 165 - 166 - """ 167 - @spec send_request(term, term) :: request_id 168 - def send_request(server, request) do 169 - case get_scheduler_pid() do 170 - nil -> 171 - :gen_server.send_request(server, request) 172 - scheduler_pid when is_pid(scheduler_pid) -> 173 - sim_send_request(scheduler_pid, server, request) 174 - end 175 - end 176 - 177 - @doc """ 178 - Receives a response from a SimServer. 179 - 180 - See `:gen_server.receive_response/2` and `SimServer.send_request/2`. 181 - """ 182 - @spec receive_response(request_id, timeout) :: {:reply, term} | :timeout 183 - def receive_response(request_id, timeout \\ 5000) do 184 - case get_scheduler_pid() do 185 - nil -> 186 - :gen_server.receive_response(request_id, timeout) 187 - scheduler_pid when is_pid(scheduler_pid) -> 188 - sim_receive_response(scheduler_pid, request_id, timeout) 189 - end 190 - |> case do 191 - {:reply, _reply} = reply -> reply 192 - # Coalesce errors into timeouts for now 193 - {:error, _err} -> :timeout 194 - :timeout -> :timeout 195 - end 196 - end 197 - 198 - defp sim_call(scheduler_pid, server, request, timeout) do 199 - alias_ref = sim_send_request(scheduler_pid, server, request) 200 - 201 - try do 202 - yield_receive(scheduler_pid, timeout) do 203 - {[:alias | ^alias_ref], reply} -> reply 204 - end 205 - catch 206 - :exit, reason -> 207 - true = Scheduler.unalias(scheduler_pid, alias_ref) 208 - receive do 209 - {[:alias | ^alias_ref], reply} -> reply 210 - after 211 - 0 -> exit({reason, {__MODULE__, :call, [server, request, timeout]}}) 212 - end 213 - end 214 - end 215 - 216 - @dialyzer {:no_improper_lists, sim_send_request: 3} 217 - defp sim_send_request(scheduler_pid, server, request) do 218 - alias_ref = Scheduler.alias(scheduler_pid) 219 - Scheduler.send(scheduler_pid, server, {:"$sim_call", {self(), [:alias | alias_ref]}, request}) 220 - 221 - alias_ref 222 - end 223 - 224 - defp sim_receive_response(scheduler_pid, request_id, timeout) when is_reference(request_id) do 225 - alias_ref = request_id 226 - # TODO: catch timeout exit 227 - try do 228 - yield_receive(scheduler_pid, timeout) do 229 - {[:alias | ^alias_ref], reply} -> {:reply, reply} 230 - end 231 - catch 232 - :exit, :timeout -> 233 - true = Scheduler.unalias(scheduler_pid, alias_ref) 234 - receive do 235 - {[:alias | ^alias_ref], reply} -> {:reply, reply} 236 - after 237 - 0 -> :timeout 238 - end 239 - end 240 - end 241 - 242 - @doc """ 243 - Performs a `SimServer` cast. 244 - 245 - ## Examples 246 - 247 - iex> SimServer.cast({:global, :my_server}, :foo} 248 - :ok 249 - 250 - """ 251 - @spec cast(term, term) :: :ok 252 - def cast(server, request) do 253 - case get_scheduler_pid() do 254 - nil -> 255 - GenServer.cast(server, request) 256 - scheduler_pid when is_pid(scheduler_pid) -> 257 - sim_cast(scheduler_pid, server, request) 258 - end 259 - end 260 - 261 - defp sim_cast(scheduler_pid, server, request) do 262 - Scheduler.send(scheduler_pid, server, {:"$sim_cast", request}) 263 - :ok 264 - end 265 - 266 - @spec reply(GenServer.from, term) :: :ok 267 - def reply(client, response) do 268 - case get_scheduler_pid() do 269 - nil -> 270 - GenServer.reply(client, response) 271 - 272 - scheduler_pid when is_pid(scheduler_pid) -> 273 - {_client_pid, [:alias | alias_ref] = tag} = client 274 - Scheduler.send(scheduler_pid, alias_ref, {tag, response}) 275 - :ok 276 - end 277 - end 278 - 279 - @spec spawn(function) :: pid 280 - def spawn(fun) when is_function(fun) do 281 - case get_scheduler_pid() do 282 - scheduler_pid when is_pid(scheduler_pid) -> 283 - Scheduler.spawn_and_yield(scheduler_pid, :nolink, Kernel, :apply, [fun, []]) 284 - nil -> 285 - Kernel.spawn(fun) 286 - end 287 - end 288 - 289 - @spec spawn_link(function) :: pid 290 - def spawn_link(fun) when is_function(fun) do 291 - case get_scheduler_pid() do 292 - scheduler_pid when is_pid(scheduler_pid) -> 293 - Scheduler.spawn_and_yield(scheduler_pid, :link, Kernel, :apply, [fun, []]) 294 - nil -> 295 - Kernel.spawn_link(fun) 296 - end 297 - end 298 - 299 - @spec sleep(non_neg_integer) :: term 300 - def sleep(time) do 301 - case get_scheduler_pid() do 302 - scheduler_pid when is_pid(scheduler_pid) -> 303 - Scheduler.yield(scheduler_pid, time) 304 - nil -> 305 - :timer.sleep(time) 306 - end 307 - end 308 - 309 - @spec send(pid, term) :: term 310 - def send(dest, message) do 311 - case get_scheduler_pid() do 312 - scheduler_pid when is_pid(scheduler_pid) -> 313 - # TODO: time=nil 314 - Scheduler.send(scheduler_pid, dest, message, 0) 315 - nil -> 316 - Kernel.send(dest, message) 317 - end 318 - end 319 - 320 - @spec send_after(pid, term, non_neg_integer) :: term 321 - def send_after(dest, message, time) do 322 - case get_scheduler_pid() do 323 - scheduler_pid when is_pid(scheduler_pid) -> 324 - Scheduler.send(scheduler_pid, dest, message, time) 325 - nil -> 326 - Process.send_after(dest, message, time) 327 - end 328 - end 329 - 330 - @spec exit(pid, term) :: term 331 - def exit(pid, reason) do 332 - case get_scheduler_pid() do 333 - scheduler_pid when is_pid(scheduler_pid) -> 334 - Scheduler.exit(scheduler_pid, pid, reason) 335 - nil -> 336 - Process.exit(pid, reason) 337 - end 338 - end 339 - 340 - @spec node :: atom 341 - def node do 342 - case get_scheduler_pid() do 343 - scheduler_pid when is_pid(scheduler_pid) -> 344 - Scheduler.get_current_node(scheduler_pid) 345 - nil -> 346 - # TODO 347 - raise "Not supported" 348 - end 349 - end 350 - 351 - @spec list_nodes :: [atom] 352 - def list_nodes do 353 - if not simulated?(), do: raise "list_nodes/0 can only be called in simulation" 354 - scheduler_pid = fetch_scheduler_pid!() 355 - 356 - Scheduler.list_nodes(scheduler_pid) 357 - end 358 - 359 - @spec start_node(atom, module, term) :: :ok 360 - def start_node(name, app_module, args) when is_atom(name) and is_atom(app_module) do 361 - if not simulated?(), do: raise "start_node/3 can only be called in simulation" 362 - scheduler_pid = fetch_scheduler_pid!() 363 - 364 - Scheduler.start_node(scheduler_pid, name, app_module, args) 365 - end 366 - 367 - @spec restart_node(atom, non_neg_integer) :: :ok | {:error, :node_not_found | :node_stopped} 368 - def restart_node(name, delay_ms \\ 0) when is_atom(name) and is_integer(delay_ms) do 369 - if not simulated?(), do: raise "restart_node/1 can only be called in simulation" 370 - scheduler_pid = fetch_scheduler_pid!() 371 - 372 - Scheduler.restart_node(scheduler_pid, name, delay_ms) 373 - end 374 - 375 - @spec monitor(pid) :: reference 376 - def monitor(monitor_pid) do 377 - case get_scheduler_pid() do 378 - scheduler_pid when is_pid(scheduler_pid) -> 379 - Scheduler.monitor(scheduler_pid, monitor_pid) 380 - nil -> 381 - Process.monitor(monitor_pid) 382 - end 383 - end 384 - 385 - @spec flag(:trap_exit, boolean) :: boolean 386 - def flag(flag, value) when flag in [:trap_exit] and is_boolean(value) do 387 - case get_scheduler_pid() do 388 - scheduler_pid when is_pid(scheduler_pid) -> 389 - Scheduler.set_process_flag(scheduler_pid, flag, value) 390 - nil -> 391 - Process.flag(flag, value) 392 - end 393 - end 394 - 395 - @spec register(pid, atom) :: true 396 - def register(pid, name) when is_pid(pid) and is_atom(name) do 397 - case get_scheduler_pid() do 398 - scheduler_pid when is_pid(scheduler_pid) -> 399 - case Scheduler.register_process(scheduler_pid, pid, name) do 400 - :ok -> true 401 - :error -> raise ArgumentError 402 - end 403 - nil -> 404 - Process.register(pid, name) 405 - end 406 - end 407 - 408 - @spec whereis(atom) :: pid | nil 409 - def whereis(name) do 410 - case get_scheduler_pid() do 411 - scheduler_pid when is_pid(scheduler_pid) -> 412 - Scheduler.whereis(scheduler_pid, name) 413 - nil -> 414 - Process.whereis(name) 415 - end 416 - end 417 - 418 - @doc """ 419 - Returns a monotonic microsecond timestamp. 420 - 421 - Outside of simulation this is UNIX time. 422 - 423 - Inside the simulation the timestamp starts at `0`. 424 - 425 - ## Examples 426 - 427 - iex> current_time() 428 - 1000 429 - 430 - iex> current_time() 431 - 1100 432 - 433 - """ 434 - @spec current_time :: non_neg_integer 435 - def current_time do 436 - case get_scheduler_pid() do 437 - scheduler_pid when is_pid(scheduler_pid) -> 438 - Scheduler.current_time(scheduler_pid) 439 - nil -> 440 - System.monotonic_time(:microsecond) 441 - end 442 - end 443 - 444 - @spec deterministic_random(Enum.t) :: Enum.element 445 - def deterministic_random(enumerable), do: Enum.random(enumerable) 446 - 447 - @spec simulate_work(integer | [integer]) :: :ok 448 - def simulate_work(delay_or_delays) do 449 - case get_scheduler_pid() do 450 - nil -> :noop 451 - pid -> do_work_delay(delay_or_delays, pid) 452 - end 453 - :ok 454 - end 455 - 456 - defp do_work_delay([_ | _] = delays, pid), do: Scheduler.yield(pid, Enum.random(delays)) 457 - defp do_work_delay(delay, pid) when is_integer(delay), do: Scheduler.yield(pid, delay) 458 - end
-60
lib/construct/sim_utils.ex
··· 1 - defmodule Hobbes.Construct.SimUtils do 2 - require Logger 3 - 4 - @doc """ 5 - Builds a printable message for the given log. 6 - 7 - Options: 8 - - `:full` - returns a full log message including PIDs and events. 9 - """ 10 - @spec build_log_message(%{log: list, known: map}, keyword) :: String.t 11 - def build_log_message(%{log: log, known: known} = log_state, opts \\ []) do 12 - pid_text = 13 - known.pids 14 - |> Enum.sort_by(fn {_k, v} -> v end) 15 - |> Enum.map(fn {k, v} -> "#{inspect(k)} -> #{inspect(v)}" end) 16 - |> Enum.join("\n") 17 - 18 - log_text = 19 - case (log_length = length(log)) > 100 do 20 - true -> 21 - first = Enum.take(log, 50) 22 - last = Enum.take(log, -50) 23 - format_log(first) <> "\n\n... #{inspect(log_length - 100)} more ...\n\n" <> format_log(last) 24 - 25 - false -> 26 - format_log(log) 27 - end 28 - 29 - hash = log_state.rolling_hash 30 - # For readability (difficult to remember integers) 31 - # sha256 results in better distribution, the bytes from phash2 are too visually similar 32 - hash_alpha = :crypto.hash(:sha256, <<hash::integer-size(32)>>) |> Base.encode32(padding: false) |> String.slice(0, 8) 33 - 34 - full_text = if opts[:full] do 35 - """ 36 - Scheduler Log: 37 - 38 - PIDs: 39 - #{pid_text} 40 - 41 - Log: 42 - #{log_text} 43 - 44 - """ 45 - else 46 - "" 47 - end 48 - 49 - full_text <> """ 50 - Logged #{log_state.num_events} events with hash #{inspect(hash)} 51 - SHA256: #{hash_alpha}\ 52 - """ 53 - end 54 - 55 - defp format_log(events) when is_list(events) do 56 - events 57 - |> Enum.map(&inspect(&1, [])) 58 - |> Enum.join("\n") 59 - end 60 - end
-368
lib/hybrid_kv.ex
··· 1 - defmodule Hobbes.HybridKV do 2 - alias Hobbes.{HybridKV, MemKV, RangeForest, Utils} 3 - alias Hobbes.RangeForest.RangeTree 4 - alias Hobbes.KV.{FlatKV, FlatStorageKV, MutationLog} 5 - alias Hobbes.Structs.RangeResult 6 - 7 - @type t :: %__MODULE__{ 8 - mem_kv: term, 9 - storage_module: module, 10 - storage_kv: FlatKV.t | FlatStorageKV.t, 11 - deleted_forest: term, 12 - mutation_log: MutationLog.t, 13 - flushed_version: non_neg_integer, 14 - } 15 - 16 - @enforce_keys [:mem_kv, :storage_module, :storage_kv, :deleted_forest, :mutation_log, :flushed_version] 17 - defstruct @enforce_keys 18 - 19 - def new(opts \\ []) do 20 - {storage_module, storage_kv} = 21 - case Keyword.get(opts, :path) do 22 - nil -> {FlatKV, FlatKV.new()} 23 - path when is_binary(path) -> {FlatStorageKV, FlatStorageKV.new(path)} 24 - end 25 - 26 - new(storage_module, storage_kv) 27 - end 28 - 29 - def new(storage_module, storage_kv) when is_atom(storage_module) do 30 - %HybridKV{ 31 - mem_kv: MemKV.new(), 32 - storage_module: storage_module, 33 - storage_kv: storage_kv, 34 - deleted_forest: RangeForest.new(), 35 - mutation_log: MutationLog.new(), 36 - flushed_version: 0, 37 - } 38 - end 39 - 40 - @spec put(%HybridKV{}, non_neg_integer, binary, binary) :: %HybridKV{} 41 - def put(%HybridKV{} = kv, version, key, value) 42 - when is_integer(version) and version >= 0 and is_binary(key) and is_binary(value) do 43 - :ok = MemKV.put(kv.mem_kv, version, key, value) 44 - 45 - case RangeForest.split_at(kv.deleted_forest, version, key) do 46 - {:updated, deleted_forest} -> %HybridKV{kv | deleted_forest: deleted_forest} 47 - :noop -> kv 48 - end 49 - end 50 - 51 - @spec delete(%HybridKV{}, non_neg_integer, binary) :: :ok 52 - def delete(%HybridKV{} = kv, version, key) when is_integer(version) and version >= 0 and is_binary(key) do 53 - :ok = MemKV.delete(kv.mem_kv, version, key) 54 - end 55 - 56 - @spec delete_range(%HybridKV{}, non_neg_integer, binary, binary) :: %HybridKV{} 57 - def delete_range(%HybridKV{} = kv, version, start_key, end_key) do 58 - %HybridKV{kv | deleted_forest: RangeForest.add_range(kv.deleted_forest, version, start_key, end_key)} 59 - end 60 - 61 - @spec get(%HybridKV{}, non_neg_integer, binary) :: binary | nil 62 - def get(%HybridKV{} = kv, version, key) 63 - when is_integer(version) and version >= 0 and is_binary(key) do 64 - kv.deleted_forest 65 - |> RangeForest.tree_at(version) 66 - |> RangeTree.intersect_key(kv.flushed_version + 1, key) 67 - |> case do 68 - nil -> 69 - # There are no overlapping range deletes 70 - case MemKV.get(kv.mem_kv, version, 0, key) do 71 - nil -> kv.storage_module.get(kv.storage_kv, key) 72 - :deleted -> nil 73 - value -> value 74 - end 75 - 76 - {_sk, _ek, range_deleted_at} -> 77 - # There is an overlapping range delete, so we return nil if 78 - # a newer key is not found 79 - case MemKV.get(kv.mem_kv, version, range_deleted_at + 1, key) do 80 - nil -> nil 81 - :deleted -> nil 82 - value -> value 83 - end 84 - end 85 - end 86 - 87 - @spec scan(%HybridKV{}, non_neg_integer, binary, binary, keyword) :: RangeResult.t 88 - def scan(%HybridKV{} = kv, version, start_key, end_key, opts \\ []) do 89 - limit = Keyword.get(opts, :limit, :infinity) 90 - reverse = Keyword.get(opts, :reverse, false) 91 - 92 - deleted_ranges = 93 - kv.deleted_forest 94 - |> RangeForest.tree_at(version) 95 - |> RangeTree.intersect_range(kv.flushed_version, start_key, end_key) 96 - 97 - read_limit = case limit do 98 - :infinity -> :infinity 99 - limit -> limit + 1 100 - end 101 - 102 - {pairs, count} = 103 - case reverse do 104 - false -> do_scan(:forward, kv, deleted_ranges, version, start_key, end_key, read_limit, 0) 105 - true -> do_scan(:backward, kv, Enum.reverse(deleted_ranges), version, start_key, end_key, read_limit, 0) 106 - end 107 - 108 - # We over-read by 1 and then use the extra read 109 - # to check if there are more 110 - cond do 111 - count < read_limit -> 112 - %RangeResult{pairs: pairs, count: count, more: false} 113 - count == read_limit -> 114 - %RangeResult{pairs: Enum.take(pairs, limit), count: limit, more: true} 115 - end 116 - end 117 - 118 - # This exists purely as a sanity check and should be unreachable except for bugs 119 - defp do_scan(_direction, _kv, _deleted_ranges, _version, _start_key, _end_key, _limit, 1000), do: raise "Scan caught in loop!" 120 - 121 - # Abandon hope all ye who enter here 122 - defp do_scan(:forward, %HybridKV{} = kv, deleted_ranges, version, start_key, end_key, limit, scan_count) do 123 - {merged_pairs, scanned_end_key, deleted_ranges} = 124 - case deleted_ranges do 125 - [{sk, ek, del_v} | rest] when sk <= start_key -> 126 - # We are "inside" a range clear, so we scan mem only to the end of min(ek, end_key) 127 - mem_result = MemKV.scan(kv.mem_kv, version, del_v + 1, start_key, min(ek, end_key), limit: limit) 128 - {_start, mem_end_key} = mem_result.range 129 - # Merge just to clear :deleted 130 - pairs = merge([], mem_result.pairs, false) 131 - 132 - # Note: it would be wrong to return "rest" here if we are still inside 133 - # the deleted range (due to hitting the limit), however if a mem scan 134 - # hits the limit then we are done scanning because there is nothing 135 - # for the tombstones to clear out, so we don't care 136 - {pairs, mem_end_key, rest} 137 - 138 - _ -> 139 - # If there is a range delete ahead, we want to stop this hybrid scan before that 140 - # delete's start_key so that the next iteration will scan it mem only (the previous clause) 141 - stop_key = case deleted_ranges do 142 - [{sk, _ek, _del_v} | _] -> sk 143 - [] -> end_key 144 - end 145 - 146 - storage_result = kv.storage_module.scan(kv.storage_kv, start_key, stop_key, limit: limit) 147 - {_start, storage_end_key} = storage_result.range 148 - 149 - # Read mem only up to the end of the range scanned by storage 150 - mem_result = MemKV.scan(kv.mem_kv, version, 0, start_key, storage_end_key, limit: limit) 151 - {_start, mem_end_key} = mem_result.range 152 - 153 - # Both KVs were scanned up to this key 154 - # Anything past this key was only scanned by storage and must be discarded 155 - scanned_end_key = min(storage_end_key, mem_end_key) 156 - 157 - merged_pairs = 158 - merge(storage_result.pairs, mem_result.pairs, false) 159 - # TODO: more efficient to do this in merge/3 160 - |> Enum.take_while(fn {k, _v} -> k < scanned_end_key end) 161 - 162 - {merged_pairs, scanned_end_key, deleted_ranges} 163 - end 164 - 165 - # TODO: compute count in merge/3 166 - count = length(merged_pairs) 167 - 168 - cond do 169 - count > limit -> 170 - # We got more pairs than we asked for, which can happen if the 171 - # storage and mem keys are disjoint (which is fairly likely in practice) 172 - {Enum.take(merged_pairs, limit), limit} 173 - 174 - count == limit -> 175 - # We got exactly what we wanted! 176 - {merged_pairs, count} 177 - 178 - count < limit -> 179 - case scanned_end_key == end_key do 180 - true -> 181 - # We got <limit pairs but we really did scan the full range 182 - {merged_pairs, count} 183 - false -> 184 - # We got <limit pairs and we have not yet scanned the full range, 185 - # so we need to keep scanning 186 - # 187 - # This could be because we hit the limit and then the :deleted 188 - # tombstones cleared out enough pairs to bring us back under, 189 - # or because we ran into a range delete and had to skip over it 190 - {next_pairs, next_count} = do_scan(:forward, kv, deleted_ranges, version, scanned_end_key, end_key, subtract_limit(limit, count), scan_count + 1) 191 - {merged_pairs ++ next_pairs, count + next_count} 192 - end 193 - end 194 - end 195 - 196 - # See :forward for comments, the :backward version is the same except key logic 197 - # is inverted (deals with start_key instead of end_key) 198 - defp do_scan(:backward, %HybridKV{} = kv, deleted_ranges, version, start_key, end_key, limit, scan_count) do 199 - {merged_pairs, scanned_start_key, deleted_ranges} = 200 - case deleted_ranges do 201 - [{sk, ek, del_v} | rest] when ek >= end_key -> 202 - mem_result = MemKV.scan(kv.mem_kv, version, del_v + 1, max(sk, start_key), end_key, limit: limit, reverse: true) 203 - {mem_start_key, _end_key} = mem_result.range 204 - pairs = merge([], mem_result.pairs, true) 205 - {pairs, mem_start_key, rest} 206 - 207 - _ -> 208 - stop_key = case deleted_ranges do 209 - [{_sk, ek, _del_v} | _] -> ek 210 - [] -> start_key 211 - end 212 - 213 - storage_result = kv.storage_module.scan(kv.storage_kv, stop_key, end_key, limit: limit, reverse: true) 214 - {storage_start_key, _end_key} = storage_result.range 215 - 216 - mem_result = MemKV.scan(kv.mem_kv, version, 0, storage_start_key, end_key, limit: limit, reverse: true) 217 - {mem_start_key, _end_key} = mem_result.range 218 - 219 - scanned_start_key = max(storage_start_key, mem_start_key) 220 - 221 - merged_pairs = 222 - merge(storage_result.pairs, mem_result.pairs, true) 223 - |> Enum.take_while(fn {k, _v} -> k >= scanned_start_key end) 224 - 225 - {merged_pairs, scanned_start_key, deleted_ranges} 226 - end 227 - 228 - count = length(merged_pairs) 229 - 230 - cond do 231 - count > limit -> 232 - {Enum.take(merged_pairs, limit), limit} 233 - 234 - count == limit -> 235 - {merged_pairs, count} 236 - 237 - count < limit -> 238 - case scanned_start_key == start_key do 239 - true -> 240 - {merged_pairs, count} 241 - false -> 242 - {next_pairs, next_count} = do_scan(:backward, kv, deleted_ranges, version, start_key, scanned_start_key, subtract_limit(limit, count), scan_count + 1) 243 - {merged_pairs ++ next_pairs, count + next_count} 244 - end 245 - end 246 - end 247 - 248 - defp subtract_limit(:infinity, _n), do: :infinity 249 - defp subtract_limit(limit, n), do: limit - n 250 - 251 - @spec merge([{binary, binary}], [{binary, binary}], boolean) :: [{binary, binary}] 252 - defp merge(list1, list2, reverse), do: do_merge(reverse, list1, list2, []) |> Enum.reverse() 253 - 254 - defp do_merge(_reverse, [], [], acc), do: acc 255 - defp do_merge(reverse, [p1 | rest1], [], acc), do: do_merge(reverse, rest1, [], acc_pair(p1, acc)) 256 - defp do_merge(reverse, [], [p2 | rest2], acc), do: do_merge(reverse, [], rest2, acc_pair(p2, acc)) 257 - 258 - defp do_merge(false, [{k1, _} = p1 | rest1] = list1, [{k2, _} = p2 | rest2] = list2, acc) do 259 - cond do 260 - k1 < k2 -> 261 - do_merge(false, rest1, list2, acc_pair(p1, acc)) 262 - k1 > k2 -> 263 - do_merge(false, list1, rest2, acc_pair(p2, acc)) 264 - k1 == k2 -> 265 - do_merge(false, rest1, rest2, acc_pair(p2, acc)) 266 - end 267 - end 268 - 269 - defp do_merge(true, [{k1, _} = p1 | rest1] = list1, [{k2, _} = p2 | rest2] = list2, acc) do 270 - cond do 271 - k1 > k2 -> 272 - do_merge(true, rest1, list2, acc_pair(p1, acc)) 273 - k1 < k2 -> 274 - do_merge(true, list1, rest2, acc_pair(p2, acc)) 275 - k1 == k2 -> 276 - do_merge(true, rest1, rest2, acc_pair(p2, acc)) 277 - end 278 - end 279 - 280 - defp acc_pair({_key, :deleted}, acc), do: acc 281 - defp acc_pair({_key, _value} = pair, acc), do: [pair | acc] 282 - 283 - @spec apply_batch(t, non_neg_integer, [Utils.mutation]) :: :ok 284 - def apply_batch(kv, version, mutations) when is_list(mutations) do 285 - MutationLog.insert(kv.mutation_log, version, mutations) 286 - 287 - Enum.reduce(mutations, kv, fn 288 - {:write, k, v}, kv -> 289 - put(kv, version, k, v) 290 - {:clear, k}, kv -> 291 - delete(kv, version, k) 292 - kv 293 - {:clear_range, sk, ek}, kv -> 294 - delete_range(kv, version, sk, ek) 295 - end) 296 - end 297 - 298 - @doc """ 299 - Flushes mutations with a version <= `version` to unversioned storage. 300 - """ 301 - @spec flush(%HybridKV{}, non_neg_integer) :: %HybridKV{} 302 - def flush(%HybridKV{storage_module: s_mod, storage_kv: s_kv} = kv, version) when is_integer(version) and version >= 0 do 303 - batches = MutationLog.pop_up_to(kv.mutation_log, version) 304 - 305 - mem_kv = kv.mem_kv 306 - Enum.each(batches, fn {ver, mutations} -> 307 - Enum.each(mutations, fn 308 - {:write, k, v} -> 309 - MemKV.remove_key_at_version(mem_kv, ver, k) 310 - s_mod.put(s_kv, k, v) 311 - 312 - {:clear, k} -> 313 - MemKV.remove_key_at_version(mem_kv, ver, k) 314 - s_mod.delete(s_kv, k) 315 - 316 - {:clear_range, sk, ek} -> 317 - MemKV.remove_range_at_version(mem_kv, ver, sk, ek) 318 - s_mod.delete_range(s_kv, sk, ek) 319 - end) 320 - end) 321 - 322 - {_ranges, deleted_forest} = RangeForest.flush(kv.deleted_forest, version, kv.flushed_version + 1) 323 - 324 - %{kv | deleted_forest: deleted_forest, flushed_version: version} 325 - end 326 - 327 - @spec put_storage(t, binary, binary) :: :ok 328 - def put_storage(%HybridKV{} = kv, key, value) when is_binary(key) and is_binary(value) do 329 - kv.storage_module.put(kv.storage_kv, key, value) 330 - end 331 - 332 - @spec commit(t) :: :ok 333 - def commit(%HybridKV{} = kv) do 334 - kv.storage_module.commit(kv.storage_kv) 335 - end 336 - 337 - @spec load_storage(%HybridKV{}, [{binary, binary}]) :: :ok 338 - def load_storage(%HybridKV{storage_module: s_mod, storage_kv: s_kv}, pairs) when is_list(pairs) do 339 - Enum.each(pairs, fn {k, v} when is_binary(k) and is_binary(v) -> 340 - s_mod.put(s_kv, k, v) 341 - end) 342 - end 343 - 344 - @spec delete_range_storage(%HybridKV{}, binary, binary) :: :ok 345 - def delete_range_storage(%HybridKV{storage_module: s_mod, storage_kv: s_kv}, start_key, end_key) do 346 - s_mod.delete_range(s_kv, start_key, end_key) 347 - end 348 - 349 - @doc """ 350 - Clears a range from both memory and storage **at all versions**. 351 - """ 352 - @spec nuke_range(%HybridKV{}, binary, binary) :: :ok 353 - def nuke_range(%HybridKV{} = kv, start_key, end_key) do 354 - MemKV.nuke_range(kv.mem_kv, start_key, end_key) 355 - delete_range_storage(kv, start_key, end_key) 356 - :ok 357 - end 358 - 359 - @doc false 360 - def dump(%HybridKV{} = kv) do 361 - %{ 362 - mem_kv: MemKV.dump(kv.mem_kv), 363 - storage_kv: kv.storage_module.dump(kv.storage_kv), 364 - deleted_forest: RangeForest.dump(kv.deleted_forest), 365 - mutation_log: MutationLog.dump(kv.mutation_log), 366 - } 367 - end 368 - end
-138
lib/kv/byte_sample.ex
··· 1 - defmodule Hobbes.KV.ByteSample do 2 - alias Hobbes.Utils 3 - import Hobbes.Utils 4 - 5 - # Byte sample should be 1/250th the size of k/v data 6 - @byte_sample_factor 250 7 - # TODO: 250 is to rare for tests, buggify 8 - #@byte_sample_factor 2 9 - 10 - # Approximate overhead per sample (other than key size) 11 - # (currently just 8 bytes to store the size value as a string) 12 - @byte_sample_overhead_bytes 8 13 - 14 - @type t :: :ets.table 15 - 16 - @spec new :: t 17 - def new do 18 - :ets.new(__MODULE__, [:ordered_set, :private]) 19 - end 20 - 21 - @spec load(t, [{binary, binary}]) :: :ok 22 - def load(table, pairs) when is_list(pairs) do 23 - Enum.each(pairs, fn {k, v} when is_binary(k) and is_binary(v) -> 24 - bytes = decode_float(v) 25 - :ets.insert(table, {k, bytes}) 26 - end) 27 - :ok 28 - end 29 - 30 - @spec delete_range(t, binary, binary) :: :ok 31 - def delete_range(table, start_key, end_key) when is_binary(start_key) and is_binary(end_key) do 32 - :ets.delete(table, start_key) 33 - do_scan_delete(table, end_key, start_key) 34 - end 35 - 36 - defp do_scan_delete(table, end_key, prev_key) do 37 - case :ets.next(table, prev_key) do 38 - key when is_binary(key) and key < end_key -> 39 - :ets.delete(table, key) 40 - do_scan_delete(table, end_key, key) 41 - 42 - _ -> :ok 43 - end 44 - end 45 - 46 - @spec scan(t, binary, binary) :: [{binary, float}] 47 - def scan(table, start_key, end_key) do 48 - acc = 49 - case :ets.lookup(table, start_key) do 50 - [{^start_key, _size}] = result -> result 51 - [] -> [] 52 - end 53 - 54 - do_scan(table, end_key, start_key, acc) 55 - |> Enum.reverse() 56 - end 57 - 58 - defp do_scan(table, end_key, prev_key, acc) do 59 - case :ets.next_lookup(table, prev_key) do 60 - {_key, [{key, _size} = pair]} -> 61 - case key < end_key do 62 - true -> do_scan(table, end_key, key, [pair | acc]) 63 - false -> acc 64 - end 65 - 66 - :"$end_of_table" -> acc 67 - end 68 - end 69 - 70 - @spec apply_batch(t, [Utils.mutation]) :: [Utils.mutation] 71 - def apply_batch(table, mutations) when is_list(mutations) do 72 - mutations 73 - |> Enum.reduce([], fn 74 - {:write, k, v}, acc -> 75 - key_size = byte_size(k) 76 - pair_size = key_size + byte_size(v) 77 - probability = byte_sample_probability(key_size, pair_size) 78 - 79 - case (:erlang.phash2(k, 1000) / 1000) < probability do 80 - true -> 81 - # Correct for sampling probability (see comments in byte_sample_probability/2) 82 - sampled_size = pair_size / min(probability, 1) 83 - 84 - :ets.insert(table, {k, sampled_size}) 85 - mut = {:write, special_byte_sample_prefix() <> k, encode_float(sampled_size)} 86 - [mut | acc] 87 - 88 - false -> acc 89 - end 90 - 91 - {:clear, k}, acc -> 92 - case :ets.member(table, k) do 93 - true -> 94 - :ets.delete(table, k) 95 - mut = {:clear, special_byte_sample_prefix() <> k} 96 - [mut | acc] 97 - 98 - false -> acc 99 - end 100 - 101 - {:clear_range, sk, ek}, acc -> 102 - delete_range(table, sk, ek) 103 - # TODO: if the range was empty in the byte sample we don't need this mutation 104 - mut = {:clear_range, special_byte_sample_prefix() <> sk, special_byte_sample_prefix() <> ek} 105 - [mut | acc] 106 - end) 107 - |> Enum.reverse() 108 - end 109 - 110 - defp byte_sample_probability(key_size, pair_size) do 111 - # Probability that a key/value pair of this size belongs in the byte sample 112 - # This is a function of the size rather than a percentage of keys so that 113 - # we can maintain the byte sample as a fraction of *total KV size* 114 - # 115 - # Intuitively: the byte sample only stores keys, so if values are larger than 116 - # the overhead we can afford to store more samples while staying under the limit 117 - # Therefore, if the value is large, probability should increase 118 - # 119 - # We then correct out the probability factor by dividing size to get sampled_size 120 - # at the end, so that the sample is not biased by the larger pairs 121 - # 122 - # This algorithm is borrowed directly from FDB (storageserver isKeyValueInSample) 123 - (pair_size / (key_size + @byte_sample_overhead_bytes)) / @byte_sample_factor 124 - end 125 - 126 - defp encode_float(float) when is_number(float) do 127 - Integer.to_string(round(float * 1000)) 128 - end 129 - 130 - defp decode_float(string) when is_binary(string) do 131 - String.to_integer(string) / 1000 132 - end 133 - 134 - @doc false 135 - def dump(table) do 136 - :ets.tab2list(table) 137 - end 138 - end
-58
lib/kv/flat_storage_kv.ex
··· 1 - defmodule Hobbes.KV.FlatStorageKV do 2 - alias Hobbes.Construct.SimFile 3 - 4 - alias Hobbes.KV.{FlatKV, FlatStorageKV} 5 - 6 - @behaviour Hobbes.KV.Storage 7 - 8 - @type t :: %__MODULE__{ 9 - path: String.t, 10 - kv: FlatKV.t, 11 - } 12 - @enforce_keys [:path, :kv] 13 - defstruct @enforce_keys 14 - 15 - @spec new(String.t) :: t 16 - def new(path) do 17 - kv = %FlatStorageKV{path: path, kv: FlatKV.new()} 18 - 19 - #case SimFile.read(path) do 20 - # {:ok, contents} -> 21 - # pairs = decode(contents) 22 - # FlatKV.load(kv.kv, pairs) 23 - 24 - # {:error, :enoent} -> :noop 25 - #end 26 - 27 - kv 28 - end 29 - 30 - def commit(%FlatStorageKV{kv: kv, path: _path}) do 31 - _contents = FlatKV.dump(kv) |> encode() 32 - #:ok = SimFile.write(path, contents) 33 - :ok 34 - end 35 - 36 - def put(%{kv: kv}, key, value), do: FlatKV.put(kv, key, value) 37 - def delete(%{kv: kv}, key), do: FlatKV.delete(kv, key) 38 - def delete_range(%{kv: kv}, start_key, end_key), do: FlatKV.delete_range(kv, start_key, end_key) 39 - 40 - def get(%{kv: kv}, key), do: FlatKV.get(kv, key) 41 - def scan(%{kv: kv}, start_key, end_key, opts \\ []), do: FlatKV.scan(kv, start_key, end_key, opts) 42 - 43 - defp encode(pairs) when is_list(pairs), do: :erlang.term_to_binary(pairs, [:deterministic]) 44 - defp decode(contents) when is_binary(contents), do: :erlang.binary_to_term(contents, [:safe]) 45 - 46 - @doc false 47 - def dump(%{kv: kv}) do 48 - FlatKV.dump(kv) 49 - end 50 - 51 - @doc false 52 - def dump_file(%{path: path}) do 53 - case SimFile.read(path) do 54 - {:ok, contents} -> decode(contents) 55 - {:error, _} -> nil 56 - end 57 - end 58 - end
-53
lib/kv/mutation_log.ex
··· 1 - defmodule Hobbes.KV.MutationLog do 2 - alias Hobbes.Utils 3 - 4 - @type t :: :ets.table 5 - 6 - @spec new :: t 7 - def new do 8 - :ets.new(__MODULE__, [:ordered_set, :private]) 9 - end 10 - 11 - @spec insert(t, non_neg_integer, [Utils.mutation]) :: :ok 12 - def insert(table, version, mutations) when is_integer(version) and is_list(mutations) do 13 - :ets.insert(table, {version, mutations}) 14 - :ok 15 - end 16 - 17 - @spec append(t, non_neg_integer, [Utils.mutation]) :: :ok 18 - def append(table, version, mutations) when is_integer(version) and is_list(mutations) do 19 - case :ets.lookup(table, version) do 20 - [{^version, existing}] -> :ets.insert(table, {version, existing ++ mutations}) 21 - [] -> :ets.insert(table, {version, mutations}) 22 - end 23 - :ok 24 - end 25 - 26 - @spec pop_up_to(t, non_neg_integer) :: [{non_neg_integer, [Utils.mutation]}] 27 - def pop_up_to(table, end_version) when is_integer(end_version) do 28 - scan_pop(table, end_version, -1, []) 29 - |> Enum.reverse() 30 - end 31 - 32 - defp scan_pop(table, end_version, prev_version, acc) do 33 - case :ets.next(table, prev_version) do 34 - ver when is_integer(ver) -> 35 - case ver <= end_version do 36 - true -> 37 - [{^ver, mutations}] = :ets.lookup(table, ver) 38 - :ets.delete(table, ver) 39 - 40 - scan_pop(table, end_version, ver, [{ver, mutations} | acc]) 41 - 42 - false -> acc 43 - end 44 - 45 - :"$end_of_table" -> acc 46 - end 47 - end 48 - 49 - @doc false 50 - def dump(table) do 51 - :ets.tab2list(table) 52 - end 53 - end
-106
lib/storage_queue.ex
··· 1 - defmodule Hobbes.StorageQueue do 2 - alias Hobbes.{StorageQueue, Utils} 3 - alias Hobbes.KV.{FlatKV, FlatStorageKV} 4 - 5 - import Hobbes.Utils 6 - 7 - @type t :: %__MODULE__{ 8 - storage_module: module, 9 - storage_kv: FlatKV.t | FlatStorageKV.t, 10 - } 11 - 12 - @enforce_keys [:storage_module, :storage_kv] 13 - defstruct @enforce_keys 14 - 15 - @spec new(module, term) :: t 16 - def new(storage_module, storage_kv) when is_atom(storage_module) do 17 - %StorageQueue{ 18 - storage_module: storage_module, 19 - storage_kv: storage_kv, 20 - } 21 - end 22 - 23 - @locked_key special_prefix() <> "/locked?" 24 - @version_key special_prefix() <> "/version" 25 - @kcv_key special_prefix() <> "/known_committed_version" 26 - 27 - @spec put_state(t, map) :: :ok 28 - def put_state(%StorageQueue{storage_module: s_mod, storage_kv: s_kv} = _sq, fields) when is_map(fields) do 29 - %{ 30 - locked?: locked?, 31 - version: version, 32 - known_committed_version: known_committed_version, 33 - } = fields 34 - 35 - s_mod.put(s_kv, @locked_key, encode_boolean(locked?)) 36 - s_mod.put(s_kv, @version_key, Integer.to_string(version)) 37 - s_mod.put(s_kv, @kcv_key, Integer.to_string(known_committed_version)) 38 - :ok 39 - end 40 - 41 - def get_state(%StorageQueue{storage_module: s_mod, storage_kv: s_kv} = _sq) do 42 - %{ 43 - locked?: s_mod.get(s_kv, @locked_key) |> decode_boolean(), 44 - version: s_mod.get(s_kv, @version_key) |> String.to_integer(), 45 - known_committed_version: s_mod.get(s_kv, @kcv_key) |> String.to_integer(), 46 - } 47 - end 48 - 49 - @spec peek_batches(t) :: [{non_neg_integer, [Utils.tagged_mutation]}] 50 - def peek_batches(%StorageQueue{} = sq) do 51 - start_key = encode_version(0) 52 - # TODO: fix after switching to binary encoding 53 - end_key = String.duplicate("9", 20) 54 - 55 - sq.storage_module.scan(sq.storage_kv, start_key, end_key) 56 - |> Map.fetch!(:pairs) 57 - |> Enum.map(fn {k, v} -> 58 - {decode_version(k), decode_mutations(v)} 59 - end) 60 - end 61 - 62 - @spec append_batch(t, non_neg_integer, [Utils.tagged_mutation]) :: :ok 63 - def append_batch(%StorageQueue{} = sq, version, tagged_mutations) when is_integer(version) and is_list(tagged_mutations) do 64 - sq.storage_module.put(sq.storage_kv, encode_version(version), encode_mutations(tagged_mutations)) 65 - end 66 - 67 - @spec pop_batches(t, non_neg_integer) :: :ok 68 - def pop_batches(%StorageQueue{} = sq, up_to_version) when is_integer(up_to_version) do 69 - start_key = encode_version(0) 70 - # Note: we need next_key/1 because up_to_version is inclusive 71 - end_key = encode_version(up_to_version) |> next_key() 72 - 73 - sq.storage_module.delete_range(sq.storage_kv, start_key, end_key) 74 - :ok 75 - end 76 - 77 - @spec commit(t) :: :ok 78 - def commit(%StorageQueue{storage_module: s_mod, storage_kv: kv}) do 79 - s_mod.commit(kv) 80 - end 81 - 82 - # TODO: binary encoding 83 - defp encode_version(version) when is_integer(version), do: Integer.to_string(version) |> String.pad_leading(20, "0") 84 - defp decode_version(string), do: String.to_integer(string) 85 - 86 - defp encode_mutations(mutations) when is_list(mutations) do 87 - :erlang.term_to_binary(mutations, [:deterministic]) 88 - end 89 - 90 - defp decode_mutations(binary) when is_binary(binary) do 91 - :erlang.binary_to_term(binary, [:safe]) 92 - |> case do 93 - mutations when is_list(mutations) -> mutations 94 - end 95 - end 96 - 97 - defp encode_boolean(boolean) when is_boolean(boolean), do: Atom.to_string(boolean) 98 - 99 - defp decode_boolean("true"), do: true 100 - defp decode_boolean("false"), do: false 101 - 102 - @doc false 103 - def dump(%StorageQueue{} = sq) do 104 - sq.storage_module.dump(sq.storage_kv) 105 - end 106 - end
-99
test/construct/scheduler/file_store_test.exs
··· 1 - defmodule Hobbes.Construct.Scheduler.FileStoreTest do 2 - use ExUnit.Case, async: true 3 - 4 - alias Hobbes.Construct.Scheduler.FileStore 5 - 6 - @moduletag :file_store 7 - 8 - setup do 9 - %{fs: FileStore.new()} 10 - end 11 - 12 - describe "mkdir/2" do 13 - test "returns error if file in path", %{fs: fs} do 14 - :ok = FileStore.mkdir(fs, "/foo") 15 - :ok = FileStore.write(fs, "/foo/bar", "hello") 16 - 17 - assert {:error, :eexist} = FileStore.mkdir(fs, "/foo/bar") 18 - assert {:error, :enoent} = FileStore.mkdir(fs, "/foo/bar/baz") 19 - end 20 - 21 - test "returns error if directory exists", %{fs: fs} do 22 - :ok = FileStore.mkdir(fs, "/foo") 23 - assert {:error, :eexist} = FileStore.mkdir(fs, "/foo") 24 - end 25 - end 26 - 27 - describe "mkdir_p/2" do 28 - test "returns error if file in path", %{fs: fs} do 29 - assert :ok = FileStore.mkdir(fs, "/foo") 30 - assert :ok = FileStore.write(fs, "/foo/file", "hello world") 31 - 32 - assert {:error, :enotdir} = FileStore.mkdir_p(fs, "/foo/file/bar") 33 - end 34 - 35 - test "makes directories", %{fs: fs} do 36 - assert :ok = FileStore.mkdir_p(fs, "/foo/bar/baz") 37 - assert :ok = FileStore.mkdir_p(fs, "/foo/bar/baz/qux") 38 - 39 - assert FileStore.dir?(fs, "/") 40 - assert FileStore.dir?(fs, "/foo") 41 - assert FileStore.dir?(fs, "/foo/bar") 42 - assert FileStore.dir?(fs, "/foo/bar/baz") 43 - assert FileStore.dir?(fs, "/foo/bar/baz/qux") 44 - assert FileStore.dir?(fs, "/foo/bar/baz/qux/") 45 - end 46 - end 47 - 48 - describe "write/3" do 49 - test "returns error if no directory", %{fs: fs} do 50 - assert {:error, :enoent} = FileStore.write(fs, "/foo/bar.txt", "hello") 51 - end 52 - 53 - test "returns error if file in path", %{fs: fs} do 54 - :ok = FileStore.write(fs, "/foo", "hello") 55 - assert {:error, :enoent} = FileStore.write(fs, "/foo/bar.txt", "hello") 56 - end 57 - 58 - test "writes file", %{fs: fs} do 59 - :ok = FileStore.mkdir_p(fs, "/foo/bar") 60 - 61 - assert :ok = FileStore.write(fs, "/foo/bar/baz.txt", "hello world!") 62 - 63 - assert {:ok, "hello world!"} = FileStore.read(fs, "/foo/bar/baz.txt") 64 - end 65 - end 66 - 67 - describe "read/2" do 68 - test "returns error if file does not exist", %{fs: fs} do 69 - assert {:error, :enoent} = FileStore.read(fs, "/foo/bar.txt") 70 - end 71 - 72 - test "returns error if file is a directory", %{fs: fs} do 73 - :ok = FileStore.mkdir_p(fs, "/foo/bar") 74 - assert {:error, :eisdir} = FileStore.read(fs, "/foo/bar") 75 - end 76 - 77 - test "reads file", %{fs: fs} do 78 - :ok = FileStore.mkdir(fs, "/foo") 79 - :ok = FileStore.write(fs, "/foo/bar.txt", "hello world!") 80 - 81 - assert {:ok, "hello world!"} = FileStore.read(fs, "/foo/bar.txt") 82 - end 83 - end 84 - 85 - describe "ls/2" do 86 - test "returns paths", %{fs: fs} do 87 - FileStore.mkdir_p(fs, "/foo/bar/a.txt") 88 - FileStore.mkdir_p(fs, "/foo/bar/b.txt") 89 - FileStore.mkdir_p(fs, "/foo/bar/c.txt") 90 - FileStore.mkdir_p(fs, "/foo/bar/baz/d.txt") 91 - 92 - assert {:ok, ["a.txt", "b.txt", "baz", "c.txt"]} = FileStore.ls(fs, "/foo/bar/") 93 - assert {:ok, ["a.txt", "b.txt", "baz", "c.txt"]} = FileStore.ls(fs, "/foo/bar") 94 - 95 - assert {:ok, ["bar"]} = FileStore.ls(fs, "/foo") 96 - assert {:ok, ["d.txt"]} = FileStore.ls(fs, "/foo/bar/baz") 97 - end 98 - end 99 - end
-68
test/construct_test.exs
··· 1 - defmodule Hobbes.ConstructTest do 2 - use ExUnit.Case, async: false 3 - 4 - alias Hobbes.Construct.{SimServer, SimFile} 5 - 6 - @moduletag :construct 7 - 8 - describe "process exits" do 9 - test "handles child process exiting normally" do 10 - SimServer.start_scheduler() 11 - 12 - SimServer.spawn(fn -> 13 - 1 + 2 14 - end) 15 - 16 - assert true 17 - end 18 - 19 - # These tests use SimServer.sleep(0) to hand off to each other, 20 - # which ensures the monitors/links connect *before* the error is raised 21 - # 22 - # Note: the :timer.sleep(1) at the end is needed to prevent error logs from being 23 - # printed even though :capture_log is true (test process otherwise seems to die too early) 24 - @tag :capture_log 25 - test "handles unlinked child process raising error" do 26 - SimServer.start_scheduler() 27 - 28 - pid = SimServer.spawn(fn -> 29 - SimServer.sleep(0) 30 - raise "Some error" 31 - end) 32 - 33 - mref = Process.monitor(pid) 34 - SimServer.sleep(0) 35 - 36 - assert_receive {:DOWN, ^mref, :process, ^pid, {%RuntimeError{message: "Some error"}, _}} 37 - :timer.sleep(1) 38 - end 39 - 40 - @tag :capture_log 41 - test "handles linked process raising error" do 42 - SimServer.start_scheduler() 43 - 44 - SimServer.flag(:trap_exit, true) 45 - 46 - pid = SimServer.spawn_link(fn -> 47 - SimServer.sleep(0) 48 - raise "Some error" 49 - end) 50 - SimServer.sleep(0) 51 - 52 - assert_receive {:EXIT, ^pid, {%RuntimeError{message: "Some error"}, _}} 53 - Process.sleep(1) 54 - end 55 - end 56 - 57 - describe "SimFile" do 58 - test "reads and writes files" do 59 - SimServer.start_scheduler() 60 - 61 - assert :ok = SimFile.mkdir_p("/foo/bar/baz") 62 - assert :ok = SimFile.write("/foo/bar/baz/qux.txt", "hello world!") 63 - 64 - assert {:ok, "hello world!"} = SimFile.read("/foo/bar/baz/qux.txt") 65 - assert {:error, :enoent} = SimFile.read("/foo/qux.txt") 66 - end 67 - end 68 - end
-197
test/hybrid_kv_test.exs
··· 1 - defmodule Hobbes.HybridKVTest do 2 - use ExUnit.Case, async: true 3 - 4 - alias Hobbes.HybridKV 5 - alias Hobbes.KV.TestKV 6 - 7 - @moduletag :hybrid_kv 8 - 9 - setup do 10 - %{kv: HybridKV.new()} 11 - end 12 - 13 - defmodule Verifier do 14 - @moduledoc """ 15 - This module is used to verify the correctness of HybridKV by checking 16 - all of its operations against a much simpler implementation (TestKV). 17 - 18 - We generate random (deterministic by seed) operations (get, put, etc) 19 - and run them through both KVs, and then check that the results match. 20 - """ 21 - 22 - alias Verifier 23 - 24 - @enforce_keys [:hybrid_kv, :test_kv, :version, :durable_version, :seed] 25 - defstruct @enforce_keys 26 - 27 - def new(seed) do 28 - :rand.seed(:exsss, seed) 29 - 30 - %Verifier{ 31 - hybrid_kv: HybridKV.new(), 32 - test_kv: TestKV.new(), 33 - version: 1, 34 - durable_version: 0, 35 - seed: seed, 36 - } 37 - end 38 - 39 - def run(%Verifier{} = verifier, op_count) do 40 - Enum.reduce(1..op_count, verifier, fn i, verifier -> 41 - #if i == 0, do: dump(verifier) 42 - 43 - try do 44 - perform(random_op(), verifier) 45 - rescue 46 - e in [ExUnit.AssertionError] -> 47 - e = Map.update!(e, :message, &(&1 <> " (at op #{i}, seed=#{inspect(verifier.seed)})")) 48 - reraise e, __STACKTRACE__ 49 - e -> 50 - require Logger 51 - Logger.error("Error #{inspect(e)} at op=#{i}, seed=#{inspect(verifier.seed)}") 52 - reraise e, __STACKTRACE__ 53 - end 54 - end) 55 - end 56 - 57 - @doc false 58 - def dump(%Verifier{} = verifier) do 59 - dbg [ 60 - hybrid_kv: HybridKV.dump(verifier.hybrid_kv), 61 - test_kv: TestKV.dump(verifier.test_kv), 62 - durable_version: verifier.durable_version, 63 - ], limit: :infinity 64 - end 65 - 66 - @ops [:apply_batch, :get, :scan] 67 - defp random_op do 68 - case Enum.random(1..100) do 69 - 1 -> :flush 70 - _ -> Enum.random(@ops) 71 - end 72 - end 73 - 74 - defp perform(:get, %Verifier{hybrid_kv: hkv, test_kv: tkv} = verifier) do 75 - read_version = rand_read_version(verifier) 76 - 77 - key = rand_hash() 78 - 79 - hkv_result = HybridKV.get(hkv, read_version, key) 80 - tkv_result = TestKV.get(tkv, read_version, key) 81 - 82 - assert hkv_result == tkv_result 83 - 84 - verifier 85 - end 86 - 87 - defp perform(:scan, %Verifier{hybrid_kv: hkv, test_kv: tkv} = verifier) do 88 - read_version = rand_read_version(verifier) 89 - 90 - start_key = rand_range_key() 91 - end_key = rand_range_key() 92 - {start_key, end_key, reverse} = 93 - cond do 94 - start_key < end_key -> {start_key, end_key, false} 95 - start_key > end_key -> {end_key, start_key, true} 96 - start_key == end_key -> {start_key, start_key <> "\x00", false} 97 - end 98 - 99 - limit = rand_limit() 100 - 101 - hkv_result = HybridKV.scan(hkv, read_version, start_key, end_key, limit: limit, reverse: reverse) 102 - tkv_result = TestKV.scan(tkv, read_version, start_key, end_key, limit: limit, reverse: reverse) 103 - 104 - assert hkv_result == tkv_result 105 - verifier 106 - end 107 - 108 - defp perform(:apply_batch, %Verifier{hybrid_kv: hkv, test_kv: tkv} = verifier) do 109 - verifier = %{verifier | version: verifier.version + Enum.random(1..1000)} 110 - 111 - mutation_count = Enum.random(1..10) 112 - mutations = Enum.reduce(1..mutation_count, [], fn _i, acc -> 113 - [rand_mutation() | acc] 114 - end) 115 - 116 - version = verifier.version 117 - hkv = HybridKV.apply_batch(hkv, version, mutations) 118 - tkv = TestKV.apply_batch(tkv, version, mutations) 119 - 120 - %{verifier | hybrid_kv: hkv, test_kv: tkv} 121 - end 122 - 123 - defp perform(:flush, %Verifier{hybrid_kv: hkv} = verifier) do 124 - flush_version = rand(verifier.durable_version, verifier.version) 125 - hkv = HybridKV.flush(hkv, flush_version) 126 - 127 - %Verifier{verifier | hybrid_kv: hkv, durable_version: flush_version} 128 - end 129 - 130 - defp rand_mutation do 131 - case Enum.random(1..10) do 132 - 1 -> 133 - start_key = rand_hash() 134 - end_key = rand_hash() 135 - {start_key, end_key} = 136 - cond do 137 - start_key < end_key -> {start_key, end_key} 138 - start_key > end_key -> {end_key, start_key} 139 - start_key == end_key -> {start_key, start_key <> "\x00"} 140 - end 141 - {:clear_range, start_key, end_key} 142 - 143 - i when i in [2, 3] -> 144 - {:clear, rand_hash()} 145 - 146 - _ -> 147 - {:write, rand_hash(), rand_hash()} 148 - end 149 - end 150 - 151 - defp rand(s \\ 0, e), do: Enum.random(s..e) 152 - 153 - defp rand_read_version(%Verifier{} = verifier) do 154 - rand(verifier.durable_version, verifier.version) 155 - end 156 - 157 - defp rand_limit do 158 - case Enum.random(1..3) do 159 - 1 -> Enum.random(2..9) 160 - 2 -> Enum.random([1, 10, 100, 1000]) 161 - 3 -> :infinity 162 - end 163 - end 164 - 165 - defp rand_range_key do 166 - case Enum.random(1..10) do 167 - 1 -> "" 168 - 2 -> "\xFF" 169 - _ -> rand_hash() 170 - end 171 - end 172 - 173 - defp rand_hash(bound \\ 99) do 174 - :crypto.hash(:sha256, Integer.to_string(rand(bound))) 175 - |> Base.encode16(padding: false) 176 - |> String.slice(0, 8) 177 - end 178 - end 179 - 180 - describe "verify HybridKV" do 181 - @tag :hkv_verify 182 - test "operations" do 183 - Verifier.new({100, 101, 102}) 184 - |> Verifier.run(1000) 185 - end 186 - 187 - @tag :hkv_verify_multi 188 - @tag :disable 189 - test "operations multi (slow)" do 190 - # 300 verification tests (takes about 1.3s) 191 - Enum.map(1..300, fn s -> 192 - Verifier.new({100 + s, 101, 102}) 193 - |> Verifier.run(1000) 194 - end) 195 - end 196 - end 197 - end
-22
test/kv/byte_sample_test.exs
··· 1 - defmodule Hobbes.KV.ByteSampleTest do 2 - use ExUnit.Case, async: true 3 - 4 - alias Hobbes.KV.ByteSample 5 - 6 - @moduletag :byte_sample 7 - 8 - setup do 9 - %{bs: ByteSample.new()} 10 - end 11 - 12 - describe "ByteSample" do 13 - test "samples", %{bs: bs} do 14 - mutations = Enum.map(1..4000, fn i -> {:write, "k#{String.pad_leading(to_string(i), 4, "0")}", "v#{i}"} end) 15 - ByteSample.apply_batch(bs, mutations) 16 - 17 - pairs = ByteSample.scan(bs, "k1000", "k2000") 18 - # Anything else will flake if we change the parameters 19 - assert is_list(pairs) 20 - end 21 - end 22 - end
-48
test/kv/mutation_log_test.exs
··· 1 - defmodule Hobbes.KV.MutationLogTest do 2 - use ExUnit.Case, async: true 3 - 4 - alias Hobbes.KV.MutationLog 5 - 6 - @moduletag :mutation_log 7 - 8 - setup do 9 - %{ml: MutationLog.new()} 10 - end 11 - 12 - describe "mutation log" do 13 - test "logs mutations", %{ml: ml} do 14 - MutationLog.insert(ml, 0, [{:write, "k0", "v0"}]) 15 - MutationLog.insert(ml, 1, [{:write, "k1", "v1"}]) 16 - MutationLog.insert(ml, 3, [{:write, "k3", "v3"}]) 17 - MutationLog.insert(ml, 4, [{:write, "k4", "v4"}]) 18 - 19 - assert MutationLog.pop_up_to(ml, 1) == [ 20 - {0, [{:write, "k0", "v0"}]}, 21 - {1, [{:write, "k1", "v1"}],}, 22 - ] 23 - assert MutationLog.pop_up_to(ml, 1) == [] 24 - assert MutationLog.pop_up_to(ml, 2) == [] 25 - 26 - assert MutationLog.pop_up_to(ml, 4) == [ 27 - {3, [{:write, "k3", "v3"}]}, 28 - {4, [{:write, "k4", "v4"}],}, 29 - ] 30 - 31 - assert MutationLog.dump(ml) == [] 32 - end 33 - 34 - test "appends", %{ml: ml} do 35 - MutationLog.append(ml, 0, [{:write, "k", "1"}]) 36 - MutationLog.append(ml, 0, [{:write, "k", "2"}]) 37 - MutationLog.append(ml, 0, [{:write, "k", "3"}]) 38 - 39 - MutationLog.insert(ml, 1, [{:write, "k", "1"}]) 40 - MutationLog.append(ml, 1, [{:write, "k", "2"}]) 41 - 42 - assert MutationLog.dump(ml) == [ 43 - {0, [{:write, "k", "1"}, {:write, "k", "2"}, {:write, "k", "3"}]}, 44 - {1, [{:write, "k", "1"}, {:write, "k", "2"}]}, 45 - ] 46 - end 47 - end 48 - end