diff --git a/lib/trinity/scheduler.ex b/lib/trinity/scheduler.ex index 177999e..ca65a14 100644 --- a/lib/trinity/scheduler.ex +++ b/lib/trinity/scheduler.ex @@ -11,6 +11,9 @@ defmodule Trinity.Scheduler do proc_nodes: :ets.table, node_procs: :ets.table, + file_paths: :ets.table, + file_data: :ets.table, + now: :atomics.atomics_ref, supervisor_pid: pid, } @@ -23,6 +26,9 @@ defmodule Trinity.Scheduler do :proc_nodes, :node_procs, + :file_paths, + :file_data, + :now, :supervisor_pid, ] diff --git a/lib/trinity/scheduler/simulation_supervisor.ex b/lib/trinity/scheduler/simulation_supervisor.ex index 4b0008f..f36f39a 100644 --- a/lib/trinity/scheduler/simulation_supervisor.ex +++ b/lib/trinity/scheduler/simulation_supervisor.ex @@ -49,10 +49,15 @@ defmodule Trinity.Scheduler.SimulationSupervisor do proc_nodes: :ets.new(__MODULE__, [:set, :public]), node_procs: :ets.new(__MODULE__, [:set, :public]), + file_paths: :ets.new(__MODULE__, [:ordered_set, :public]), + file_data: :ets.new(__MODULE__, [:ordered_set, :public]), + now: :atomics.new(1, signed: false), supervisor_pid: self(), } + :ets.insert(sim.file_paths, {:next_fd, 0}) + # TODO: configurable? root_node = :nonode root_pid = spawn_sim_child(sim, fn -> diff --git a/lib/trinity/sim_file.ex b/lib/trinity/sim_file.ex new file mode 100644 index 0000000..59fcb4a --- /dev/null +++ b/lib/trinity/sim_file.ex @@ -0,0 +1,151 @@ +defmodule Trinity.SimFile do + alias Trinity.Scheduler.Simulation + import Trinity.Scheduler, only: [simulation_key: 0, sim_node_key: 0] + + @spec get_sim :: Simulation.t | nil + defp get_sim, do: Process.get(simulation_key()) + + @spec get_proc_node :: atom + defp get_proc_node, do: Process.get(sim_node_key()) + + @spec open(String.t, [atom]) :: term + def open(path, modes) do + case get_sim() do + nil -> raise "not supported" + _sim -> sim_open(path, modes) + end + end + + def pread(fd, loc, bytes) do + case get_sim() do + nil -> raise "not supported" + _sim -> sim_pread(fd, loc, bytes) + end + end + + @spec pwrite(term, non_neg_integer, binary) :: :ok | {:error, term} + def pwrite(fd, loc, bytes) do + case get_sim() do + nil -> raise "not supported" + _sim -> sim_pwrite(fd, loc, bytes) + end + end + + defp next_fd(file_paths), do: :ets.update_counter(file_paths, :next_fd, 1) + + defp sim_open(path, _modes) do + %{file_paths: file_paths} = get_sim() + node = get_proc_node() + + key = {node, path} + case :ets.lookup(file_paths, key) do + [{_path, fd}] -> + {:ok, fd} + [] -> + # TODO: check directory + fd = next_fd(file_paths) + :ets.insert(file_paths, {key, fd}) + {:ok, fd} + end + end + + # TODO: much larger + @block_size 4 + + defp sim_pread(fd, loc, bytes) do + %{file_data: file_data} = get_sim() + + start_block_index = div(loc, @block_size) + start_pos = rem(loc, @block_size) + + << + _prefix::binary-size(start_pos), + bin_acc::binary, + >> = read_block(file_data, fd, start_block_index) + + bytes_remaining = bytes - byte_size(bin_acc) + do_read_blocks(file_data, fd, start_block_index + 1, bin_acc, bytes_remaining) + end + + defp do_read_blocks(_file_data, _fd, _block_index, bin_acc, 0) do + bin_acc + end + + defp do_read_blocks(file_data, fd, block_index, bin_acc, bytes_remaining) when bytes_remaining < @block_size do + << + bin::binary-size(bytes_remaining), + _rest::binary, + >> = read_block(file_data, fd, block_index) + <> + end + + defp do_read_blocks(file_data, fd, block_index, bin_acc, bytes_remaining) do + bin = read_block(file_data, fd, block_index) + bin_acc = <> + do_read_blocks(file_data, fd, block_index + 1, bin_acc, bytes_remaining - @block_size) + end + + defp read_block(file_data, fd, block_index) do + case :ets.lookup(file_data, {fd, block_index}) do + [{_key, existing}] -> existing + [] -> <<0::integer-unit(8)-size(@block_size)>> + end + end + + defp sim_pwrite(fd, loc, bin) do + %{file_data: file_data} = get_sim() + + bin_size = byte_size(bin) + start_block_index = div(loc, @block_size) + start_pos = rem(loc, @block_size) + + case (loc + bin_size) <= @block_size do + true -> + write_block(file_data, fd, start_block_index, start_pos, bin) + + false -> + << + start_bin::binary-size(@block_size - start_pos), + rest::binary, + >> = bin + write_block(file_data, fd, start_block_index, start_pos, start_bin) + do_write_blocks(rest, file_data, fd, start_block_index + 1) + end + :ok + end + + defp do_write_blocks("", _file_data, _fd, _block_index) do + :ok + end + + defp do_write_blocks(<>, file_data, fd, block_index) do + write_block(file_data, fd, block_index, 0, bin) + do_write_blocks(rest, file_data, fd, block_index + 1) + end + + defp do_write_blocks(<>, file_data, fd, block_index) do + # Last block may be smaller than block_size + write_block(file_data, fd, block_index, 0, bin) + :ok + end + + # TODO: handle case where (pos == 0 and byte_size(bin) == block_size) more efficiently + defp write_block(file_data, fd, block_index, pos, bin) do + key = {fd, block_index} + existing_data = case :ets.lookup(file_data, {fd, block_index}) do + [{_key, existing}] -> existing + [] -> <<0::integer-unit(8)-size(@block_size)>> + end + + << + prefix::binary-size(pos), + _::binary-size(byte_size(bin)), + suffix::binary, + >> = existing_data + + # Copy is not needed here due to construction with prefix/suffix + # TODO: confirm the above + new_data = <> + :ets.insert(file_data, {key, new_data}) + end +end diff --git a/test/trinity_test.exs b/test/trinity_test.exs index fcb4b39..d0d4c9a 100644 --- a/test/trinity_test.exs +++ b/test/trinity_test.exs @@ -6,18 +6,42 @@ defmodule TrinityTest do defmodule Counter do use GenServer - alias Trinity.SimServer + alias Trinity.{SimServer, SimFile} - def start_link(initial_count), do: SimServer.start_link(__MODULE__, initial_count) - def add(server, amount), do: SimServer.call(server, {:add, amount}) + def start_link(id, initial_count) do + SimServer.start_link(__MODULE__, %{id: id, initial_count: initial_count}) + end + + def add(server, amount) do + SimServer.call(server, {:add, amount}) + end + + def init(%{id: id, initial_count: initial_count}) do + state = %{ + fd: SimFile.open("/counters/#{id}.count", [:read, :write]), + size: nil, + } + state = write_value(state, initial_count) + {:ok, state} + end + + def handle_call({:add, amount}, _from, state) do + value = read_value(state) + value = value + amount + state = write_value(state, value) + {:reply, value, state} + end - def init(initial_count) do - {:ok, initial_count} + defp read_value(%{fd: fd, size: size}) do + data = SimFile.pread(fd, 3, size) + "The current value of the counter is: " <> value_str = data + String.to_integer(value_str) end - def handle_call({:add, amount}, _from, count) do - count = count + amount - {:reply, count, count} + defp write_value(%{fd: fd} = state, value) do + data = "The current value of the counter is: " <> Integer.to_string(value) + SimFile.pwrite(fd, 3, data) + %{state | size: byte_size(data)} end end @@ -38,8 +62,8 @@ defmodule TrinityTest do end pids = Enum.map(1..10, fn i -> - {:ok, pid} = Counter.start_link(i) - pid + {:ok, pid} = Counter.start_link(i, i) + {pid, i} end) SimProcess.send parent, {ref, pids} @@ -55,12 +79,13 @@ defmodule TrinityTest do dbg pids - Enum.each(pids, fn pid -> - dbg Counter.add(pid, 10) + Enum.each(pids, fn {pid, id} -> + result = Counter.add(pid, 10) + assert result == (id + 10) end) Scheduler.yield(1000) - dbg Scheduler.dump(), limit: :infinity + #dbg Scheduler.dump(), limit: :infinity end) end end