this repo has no description
2
fork

Configure Feed

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

Add write conflict range support to VersionMap

garrison d344adb8 bd132dbc

+97 -19
+1
ROADMAP.md
··· 7 7 - [X] BeginBuffer server (batch get_read_version requests) 8 8 - [X] Use slots to spawn servers 9 9 - [ ] Range clears 10 + - [X] Support write conflict ranges in VersionMap (decreased bench_rw from 28k to 24k TPS) 10 11 - [ ] Add clear_range() to Transaction 11 12 - [ ] Slice range clear mutations in CommitBuffer 12 13 - [ ] Apply range clears on Storage servers
+82 -14
lib/version_map.ex
··· 7 7 VersionMap is backed by an `:ets` table. 8 8 """ 9 9 10 + import Hobbes.Utils 11 + 10 12 @opaque t :: :ets.table 11 13 12 14 @doc """ ··· 28 30 """ 29 31 @spec add_writes(VersionMap.t, non_neg_integer, [binary]) :: :ok 30 32 def add_writes(table, version, keys) when is_integer(version) and is_list(keys) do 31 - Enum.each(keys, fn k when is_binary(k) -> 32 - true = :ets.insert(table, {k, version}) 33 + Enum.each(keys, fn 34 + {_sk, _ek} -> :noop 35 + k -> add_range(table, version, k, next_key(k)) 33 36 end) 34 37 :ok 35 38 end 36 39 40 + defp add_range(table, version, start_key, end_key) do 41 + prev_key = 42 + case :ets.prev_lookup(table, start_key) do 43 + {_, [{sk, {ek, ver}}]} -> 44 + cond do 45 + ek <= start_key -> 46 + # The previous range does not intersect with this range 47 + :noop 48 + ek <= end_key -> 49 + # The previous range intersects with this range and must be trimmed to {sk, start_key} 50 + :ets.insert(table, {sk, {start_key, ver}}) 51 + ek > end_key -> 52 + # The previous range completely envelops this range and must be split into {sk, start_key} and {end_key, ek} 53 + :ets.insert(table, {sk, {start_key, ver}}) 54 + :ets.insert(table, {end_key, {ek, ver}}) 55 + end 56 + 57 + sk 58 + 59 + :"$end_of_table" -> 60 + # There is no previous range 61 + nil 62 + end 63 + 64 + case clear_between(table, end_key, prev_key) do 65 + {sk, {ek, ver}} -> 66 + case sk < end_key do 67 + true -> 68 + # The last range of the clear intersects with the end of this range and must be trimmed to {end_key, ek} 69 + :ets.delete(table, sk) 70 + :ets.insert(table, {end_key, {ek, ver}}) 71 + 72 + false -> :noop 73 + end 74 + 75 + nil -> :noop 76 + end 77 + 78 + :ets.insert(table, {start_key, {end_key, version}}) 79 + end 80 + 81 + defp clear_between(table, end_key, prev_key) do 82 + case :ets.next_lookup(table, prev_key) do 83 + {_, [{sk, {ek, ver}}]} -> 84 + case ek <= end_key do 85 + true -> 86 + :ets.delete(table, sk) 87 + clear_between(table, end_key, sk) 88 + false -> 89 + {sk, {ek, ver}} 90 + end 91 + 92 + :"$end_of_table" -> 93 + nil 94 + end 95 + end 96 + 37 97 @doc """ 38 98 Checks if a key or key range has been written to after `version`. 39 99 ··· 50 110 def written_after?(table, version, key_or_range) 51 111 52 112 def written_after?(table, version, key) when is_integer(version) and is_binary(key) do 53 - case :ets.lookup(table, key) do 54 - [{^key, ver}] -> ver > version 55 - [] -> false 113 + case :ets.prev_lookup(table, next_key(key)) do 114 + {_, [{_sk, {ek, ver}}]} -> ver > version and key < ek 115 + :"$end_of_table" -> false 56 116 end 57 117 end 58 118 59 119 @spec written_after?(VersionMap.t, non_neg_integer, {binary, binary}) :: boolean 60 120 def written_after?(table, version, {start_key, end_key}) 61 121 when is_integer(version) and is_binary(start_key) and is_binary(end_key) do 62 - # TODO: inclusive/exclusive on both sides for reverse range reads 63 122 # Check first key, then scan 64 123 case written_after?(table, version, start_key) do 65 124 true -> true ··· 69 128 70 129 defp do_written_after_scan(table, version, prev_key, end_key) do 71 130 case :ets.next_lookup(table, prev_key) do 72 - {_next_key, [{next_key, ver}]} when next_key < end_key -> 73 - case ver > version do 74 - true -> true 75 - false -> do_written_after_scan(table, version, next_key, end_key) 131 + {_, [{sk, {_ek, ver}}]} -> 132 + case sk < end_key do 133 + true -> 134 + # Range intersects, check for a conflict 135 + case ver > version do 136 + # Conflict found 137 + true -> true 138 + # Range does not conflict, keep scanning 139 + false -> do_written_after_scan(table, version, sk, end_key) 140 + end 141 + 142 + # Range does not intersect, scan complete 143 + false -> false 76 144 end 77 145 78 - _ -> 79 - false 146 + # Reached the end of the table without finding a conflict 147 + :"$end_of_table" -> false 80 148 end 81 149 end 82 150 ··· 92 160 @spec clear_old(VersionMap.t, non_neg_integer) :: :ok 93 161 def clear_old(table, up_to_version) when is_integer(up_to_version) and up_to_version >= 0 do 94 162 :ets.select_delete(table, [{ 95 - {:"$1", :"$2"}, 96 - [{:"=<", :"$2", up_to_version}], 163 + {:"$1", {:"$2", :"$3"}}, 164 + [{:"=<", :"$3", up_to_version}], 97 165 [true], 98 166 }]) 99 167 :ok
+6
test/hobbes_test.exs
··· 235 235 use ExUnit.Case, async: true 236 236 @tag :bench_rw 237 237 @tag :disable 238 + # Current results: 239 + # 10s: 240 + # 249,400 transactions (24,600 TPS) 241 + # 242 + # 10s (resolver disabled): 243 + # 398,500 transactions (39,500 TPS) 238 244 test "ReadWrite", %{test: test} do 239 245 Workloads.run([ 240 246 {Workloads.ReadWrite, [
+8 -5
test/version_map_test.exs
··· 3 3 4 4 alias Hobbes.VersionMap 5 5 6 - @moduletag :version_map 7 - 8 6 describe "VersionMap" do 7 + @describetag :version_map 9 8 setup do 10 9 %{vm: VersionMap.new()} 11 10 end ··· 56 55 # Old versions are cleared every so often to simulate the MVCC window 57 56 # 58 57 # Current results (1,000,000 batches) 59 - # 50/50 (10r, 10w): 26.5s ( 37,700 TPS) 60 - # 90/10 (10r, 1w): 6.1s (162,200 TPS) 61 - # 10/90 ( 1r, 10w): 18.0s ( 55,400 TPS) 58 + # 50/50 (10r, 10w): 40.2s ( 24,800 TPS) 59 + # 90/10 (10r, 1w): 9.1s (110,200 TPS) 60 + # 10/90 ( 1r, 10w): 27.9s ( 35,800 TPS) 61 + # 50/50 ( 1r, 1w): 2.5s (392,800 TPS) 62 + # 63 + # 10,000,000 batches 64 + # 50/50 ( 1r, 1w): 27.9s (357,800 TPS) 62 65 batch_count = 1_000_000 63 66 writes_per_batch = 10 64 67 reads_per_batch = 10