Skip to content

Commit a2ab292

Browse files
feat: expand sequence marker window size and add note
1 parent 4b35387 commit a2ab292

2 files changed

Lines changed: 37 additions & 27 deletions

File tree

‎ibmcloudant/features/changes_follower.py‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
# before the client timeout it is set to 3 seconds less.
4545
_LONGPOLL_TIMEOUT = _MIN_CLIENT_TIMEOUT - 3000
4646
_BATCH_SIZE = 10000
47-
_SEQ_MARKERS_CAPACITY = 200
48-
_SEQ_MARKERS_EVICTION_COUNT = _SEQ_MARKERS_CAPACITY // 10
47+
_SEQ_RETAINED_SIZE = 2048
48+
_SEQ_EVICTION_COUNT = _SEQ_RETAINED_SIZE // 2
4949

5050
# Base delay in milliseconds between unsuccessful attempts to pull changes feed
5151
# in presence of transient errors
@@ -62,12 +62,12 @@ class _Mode(Enum):
6262
LISTEN = auto()
6363

6464

65-
class _SeqEntryType(Enum):
65+
class _SeqMarkerType(Enum):
6666
"""
6767
Enum for the type of a seq marker entry tracked by the changes follower.
6868
"""
6969
ROW = auto()
70-
PAGE = auto()
70+
LAST = auto()
7171

7272

7373
class _TransientErrorSuppression(Enum):
@@ -194,7 +194,7 @@ def last_seq_since(self, last_persisted_seq: str) -> str:
194194
result = None
195195
for entry in markers:
196196
if found:
197-
if entry['type'] == _SeqEntryType.ROW:
197+
if entry['seqMarkerType'] == _SeqMarkerType.ROW:
198198
break
199199
if entry['seq'] is not None:
200200
result = entry['seq']
@@ -207,16 +207,16 @@ def _update_seq_markers(self, results: list, last_seq: str) -> None:
207207
"""
208208
Update the seq markers list with entries from a completed page.
209209
210-
Evicts the oldest entries if the list is at capacity, then appends
211-
a ROW entry for the last change item (if any) and a PAGE entry for
212-
the page's last_seq.
210+
Bulk-evicts the oldest entries if the list is at or above the eviction
211+
threshold, then appends a ROW entry for the last change item (if any)
212+
and a LAST entry for the page's last_seq.
213213
"""
214214
with self._seq_markers_lock:
215-
if len(self._seq_markers) >= _SEQ_MARKERS_CAPACITY:
216-
del self._seq_markers[:_SEQ_MARKERS_EVICTION_COUNT]
215+
if len(self._seq_markers) >= (_SEQ_RETAINED_SIZE + _SEQ_EVICTION_COUNT):
216+
del self._seq_markers[:_SEQ_EVICTION_COUNT]
217217
if len(results) > 0:
218-
self._seq_markers.append({'type': _SeqEntryType.ROW, 'seq': results[-1].get('seq')})
219-
self._seq_markers.append({'type': _SeqEntryType.PAGE, 'seq': last_seq})
218+
self._seq_markers.append({'seqMarkerType': _SeqMarkerType.ROW, 'seq': results[-1].get('seq')})
219+
self._seq_markers.append({'seqMarkerType': _SeqMarkerType.LAST, 'seq': last_seq})
220220

221221
def _request_callback(self):
222222
while True:
@@ -501,6 +501,12 @@ def latest_sequence_from(self, checkpoint_sequence_id: str) -> str:
501501
non-``None`` ``seq`` and persist the returned value to use as the
502502
``since`` parameter for the next run.
503503
504+
Note: the follower retains a bounded window of recent sequence IDs.
505+
If subsequent page fetches displace the supplied
506+
``checkpoint_sequence_id`` from that window, this method returns
507+
the supplied value unchanged. Call this method and persist its result
508+
regularly to obtain a fresh sequence ID.
509+
504510
:param str checkpoint_sequence_id: The last checkpoint sequence ID
505511
— either the non-``None`` ``seq`` of the last ``ChangesResultItem``
506512
fully processed, or a value previously returned by this method.

‎test/unit/features/test_changes_follower.py‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
_BATCH_SIZE,
3434
_FOREVER,
3535
_LONGPOLL_TIMEOUT,
36-
_SEQ_MARKERS_CAPACITY,
37-
_SEQ_MARKERS_EVICTION_COUNT,
36+
_SEQ_RETAINED_SIZE,
37+
_SEQ_EVICTION_COUNT,
3838
ChangesFollower,
3939
_Mode,
4040
)
@@ -806,7 +806,7 @@ def test_last_seq_since_followed_by_non_empty(self):
806806

807807
# -----------------------------------------------------------------------
808808
# Per-page-type: followed by an empty page (type 9 at base 20)
809-
# Page 2 inserts only PAGE('20-aa') — no ROW to block, advances to '20-aa'.
809+
# Page 2 inserts only LAST('20-aa') — no ROW to block, advances to '20-aa'.
810810
# -----------------------------------------------------------------------
811811

812812
def test_last_seq_since_followed_by_empty(self):
@@ -830,7 +830,7 @@ def test_last_seq_since_followed_by_empty(self):
830830

831831
# -----------------------------------------------------------------------
832832
# All 8 three-page sequences of empty (E=type 9) and non-empty (N=type 1).
833-
# Query from page 1's last_seq key. E adds only PAGE; N adds ROW+PAGE.
833+
# Query from page 1's last_seq key. E adds only LAST; N adds ROW+LAST.
834834
# -----------------------------------------------------------------------
835835

836836
def test_last_seq_since_3_page_sequence(self):
@@ -860,23 +860,27 @@ def test_last_seq_since_3_page_sequence(self):
860860

861861
# -----------------------------------------------------------------------
862862
# Eviction
863-
# Each non-empty page (type 2) adds 2 entries (ROW + PAGE).
864-
# With CAPACITY=200 and EVICTION_COUNT=20, adding 101 pages triggers one
865-
# eviction of the oldest 20 entries (first 10 pages).
863+
# Each non-empty page (type 2) adds 2 entries (ROW + LAST). Adding enough
864+
# pages to exceed RETAINED_SIZE + EVICTION_COUNT entries triggers one
865+
# eviction of the oldest EVICTION_COUNT entries.
866866
# Entries for page 0 (base=0) should be gone; most recent should remain.
867867
# -----------------------------------------------------------------------
868868

869869
def test_last_seq_since_eviction(self):
870-
pages = [_page_type(2, i * 10) for i in range(101)]
870+
entries_per_non_empty_page = 2
871+
target_entries = _SEQ_RETAINED_SIZE + _SEQ_EVICTION_COUNT + entries_per_non_empty_page
872+
page_count = target_entries // entries_per_non_empty_page
873+
pages = [_page_type(2, i * 10) for i in range(page_count)]
871874
iterator = _populate_iterator(pages)
872875

873-
# Page 0 (base=0): row=_seq(1), page=_seq(2) — evicted
876+
# Page 0 (base=0): row=_seq(1), last=_seq(2) — evicted
874877
self.assertEqual(iterator.last_seq_since(_seq(1)), _seq(1))
875878
self.assertEqual(iterator.last_seq_since(_seq(2)), _seq(2))
876879

877-
# Most recent page (base=1000): row=_seq(1001), page=_seq(1002) — still present
878-
self.assertEqual(iterator.last_seq_since(_seq(1001)), _seq(1002))
879-
self.assertEqual(iterator.last_seq_since(_seq(1002)), _seq(1002))
880+
# Most recent page (base=(page_count-1)*10): row and last — still present
881+
last_base = (page_count - 1) * 10
882+
self.assertEqual(iterator.last_seq_since(_seq(last_base + 1)), _seq(last_base + 2))
883+
self.assertEqual(iterator.last_seq_since(_seq(last_base + 2)), _seq(last_base + 2))
880884

881885
# -----------------------------------------------------------------------
882886
# None seq row (seq_interval scenario) — skipped, returns input unchanged
@@ -929,9 +933,9 @@ def test_last_seq_since_consecutive_none_last_seq_pages(self):
929933

930934
def test_last_seq_since_none_last_seq_advances_beyond(self):
931935
"""
932-
p1 (type 9, base=10): PAGE('10-aa')
933-
p2 (None last_seq): PAGE(None) — skipped, scan continues
934-
p3 (type 9, base=30): PAGE('30-aa') — advances to here
936+
p1 (type 9, base=10): LAST('10-aa')
937+
p2 (None last_seq): LAST(None) — skipped, scan continues
938+
p3 (type 9, base=30): LAST('30-aa') — advances to here
935939
"""
936940
none_page = {'results': [], 'last_seq': None, 'pending': 0}
937941
pages = [_page_type(9, 10), none_page, _page_type(9, 30)]

0 commit comments

Comments
 (0)