commitkv 1.0.1__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commitkv
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  License-File: LICENSE
5
5
  Summary: Compressed key-value store for git-managed caches: append-only segments that never exceed a size limit
6
6
  License-Expression: Apache-2.0
@@ -80,6 +80,14 @@ store.refresh(); // pick up what other stores wrote to the directory
80
80
  writing (the first `put`, and the first after a failed one); records that other stores write
81
81
  stay invisible until one of those happens. Call `refresh` after
82
82
  replacing segments under an open store, for example by checking out another branch.
83
+ - Stores that write to a directory at the same time each append to a segment of their own, and
84
+ the next run continues at most one of them. Open one store per directory in a process and share
85
+ it; a store per use leaves a small segment behind on every run, and a small segment
86
+ compresses worse because its values have few earlier ones to refer to.
87
+ - `keys` lists the keys in the order their current values lie in the segments, oldest segment
88
+ first. One store's writes are listed in the order it made them; the writes of several stores
89
+ are grouped by segment, so their order does not tell which was made first. Read every value
90
+ in that order: a scattered order decodes the surrounding values again for most of them.
83
91
  - Among records of a key that stores wrote without seeing each other's, which one wins is
84
92
  unspecified, but every reader picks the same one.
85
93
  - `max_segment_bytes` / `maxSegmentBytes` must be between 1,024 and 100,000,000; the upper
@@ -69,6 +69,14 @@ store.refresh(); // pick up what other stores wrote to the directory
69
69
  writing (the first `put`, and the first after a failed one); records that other stores write
70
70
  stay invisible until one of those happens. Call `refresh` after
71
71
  replacing segments under an open store, for example by checking out another branch.
72
+ - Stores that write to a directory at the same time each append to a segment of their own, and
73
+ the next run continues at most one of them. Open one store per directory in a process and share
74
+ it; a store per use leaves a small segment behind on every run, and a small segment
75
+ compresses worse because its values have few earlier ones to refer to.
76
+ - `keys` lists the keys in the order their current values lie in the segments, oldest segment
77
+ first. One store's writes are listed in the order it made them; the writes of several stores
78
+ are grouped by segment, so their order does not tell which was made first. Read every value
79
+ in that order: a scattered order decodes the surrounding values again for most of them.
72
80
  - Among records of a key that stores wrote without seeing each other's, which one wins is
73
81
  unspecified, but every reader picks the same one.
74
82
  - `max_segment_bytes` / `maxSegmentBytes` must be between 1,024 and 100,000,000; the upper
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "commitkv"
3
- version = "1.0.1"
3
+ version = "1.1.0"
4
4
  description = "Compressed key-value store for git-managed caches: append-only segments that never exceed a size limit"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -25,6 +25,9 @@ class Store:
25
25
 
26
26
  def __contains__(self, key: str | bytes) -> bool: ...
27
27
  def __len__(self) -> int: ...
28
- def keys(self) -> list[bytes]: ...
28
+ def keys(self) -> list[bytes]:
29
+ """Lists the keys in the order their current values lie in the segments, oldest segment
30
+ first, which is the order one store wrote them; reading every value is fastest in this
31
+ order."""
29
32
  def refresh(self) -> None:
30
33
  """Picks up records that other stores wrote to the directory."""
@@ -214,9 +214,14 @@ impl Store {
214
214
  self.len() == 0
215
215
  }
216
216
 
217
+ /// The keys in the order their current records lie in the segments, oldest segment first,
218
+ /// so reading every value in this order decodes each group once. This is the order one
219
+ /// store wrote them; stores that write at the same time fill segments of their own.
217
220
  pub fn keys(&self) -> Vec<Vec<u8>> {
218
221
  let state = self.read_state();
219
- state.index.keys().map(|key| key.to_vec()).collect()
222
+ let mut entries: Vec<_> = state.index.iter().collect();
223
+ entries.sort_unstable_by_key(|(_, location)| state.position(location));
224
+ entries.into_iter().map(|(key, _)| key.to_vec()).collect()
220
225
  }
221
226
 
222
227
  /// Picks up records that other stores wrote to the directory since the last call, and
@@ -611,6 +616,12 @@ impl State {
611
616
  index
612
617
  }
613
618
 
619
+ /// Orders records by age: segment names order by creation time and offsets order within a
620
+ /// segment.
621
+ fn position(&self, location: &Location) -> (&str, u64) {
622
+ (&self.segments[location.segment].name, location.end)
623
+ }
624
+
614
625
  /// Adds a record that follows the scanned part of its segment; `false` when the record
615
626
  /// cannot be placed, which leaves the state unchanged.
616
627
  fn apply(&mut self, segment_index: usize, record: &Record) -> bool {
@@ -648,12 +659,11 @@ impl State {
648
659
  segment.scanned = location.end;
649
660
  segment.group_raw = group_raw_end;
650
661
  segment.raw_total = raw_total;
651
- // The newest record of a key wins: segment names order by creation time and offsets
652
- // order within a segment.
653
- let newer = self.index.get(record.key).is_none_or(|current| {
654
- (&self.segments[current.segment].name, current.end)
655
- < (&self.segments[segment_index].name, location.end)
656
- });
662
+ // The newest record of a key wins.
663
+ let newer = self
664
+ .index
665
+ .get(record.key)
666
+ .is_none_or(|current| self.position(current) < self.position(&location));
657
667
  if newer {
658
668
  self.index.insert(record.key.into(), location);
659
669
  }
@@ -55,6 +55,21 @@ fn values_survive_reopening_and_the_latest_value_of_a_key_wins() {
55
55
  assert!(keys.binary_search(&b"large".to_vec()).is_ok());
56
56
  }
57
57
 
58
+ #[test]
59
+ fn keys_are_listed_in_the_order_a_store_wrote_their_current_values() {
60
+ let dir = tempfile::tempdir().unwrap();
61
+ {
62
+ let store = Store::open(dir.path(), small_segments()).unwrap();
63
+ for i in 0..400 {
64
+ store.put(&key(i), &noise(i as u64, 1000)).unwrap();
65
+ }
66
+ store.put(&key(3), b"replaced").unwrap();
67
+ }
68
+ assert!(segment_paths(dir.path()).len() > 1);
69
+ let expected: Vec<Vec<u8>> = (0..400).filter(|&i| i != 3).chain([3]).map(key).collect();
70
+ assert_eq!(open(dir.path()).keys(), expected);
71
+ }
72
+
58
73
  #[test]
59
74
  fn no_segment_exceeds_the_limit() {
60
75
  let dir = tempfile::tempdir().unwrap();
File without changes
File without changes
File without changes