Skip to content

feat(state,parse): Perform partial updates on StopEvent - #1065

Open
runkelcorey wants to merge 5 commits into
masterfrom
feat-stop-event-partial-updates
Open

feat(state,parse): Perform partial updates on StopEvent#1065
runkelcorey wants to merge 5 commits into
masterfrom
feat-stop-event-partial-updates

Conversation

@runkelcorey

@runkelcorey runkelcorey commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Asana Ticket: 🐞 State.StopEvent times out on refresh

What changes does this PR propose?

Changes refresh strategy for StopEvent from a full load to an incremental update by:

  1. Implementing handle_new_state callback that filters incoming stop event JSONs for records that are newer than a configurable timestamp (defaulting to 2 hours, the same that Flashback aims for)
  2. Implementing post_commit_hook callback that evicts records from the mnesia table if they are older than the configurable timestamp

Since StopEvent is a :set table, incoming values with the same keys will overwrite existing values.

How were these changes validated?

  1. Added tests to State.StopEvent and Parse.StopEvent that different cases of partial updates
  2. Deployed to dev-blue and saw dramatic differences between the refresh times in dev-blue and dev-green (which is running the current, full-load strategy):
measure dev-green dev-blue 𝚫
5th percentile 15,681ms 11ms -99.9%
median 17,459ms 14ms -99.9%
95th percentile 19,957ms 27ms -99.9%

These are crazy improvements so I feel like I'm missing a logging statement to capture the full overhead of the partial load. However, we should expect something like this: of the 116,740 events in the stop events dataset I downloaded yesterday, only 356 (0.3%) were updated or inserted the last 30 seconds of the dataset.

distribution of stop events over time

As a sanity check, I also counted the number of times each of these function were executed: since this is an event loop dictated by the time it takes to refresh data + sleep time, the more times something is invoked can indicate performance impacts. dev-blue completed 1,059 refreshes of StopEvent in 30 minutes, which translates to once per 1.7s, while dev-green completed 432 refreshes in the same time period, a rate of one refresh every 4s.

Performance-wise, these changes seem very impactful.

What questions should reviewers consider?

  1. This logic duplicates how we create the dataset in Lamp: upsert new records and evict old ones; is this a sign that the API is doing too much?
  2. Do you think that the logic for a full load is really just a special case of a partial load?
  3. Stylistically, should timestamp be a datetime or is it ok to have as a Unix timestamp?
  4. Is there an better way to evict that would work better here? Like, to fully embrace the actor model, I imagine having each key track its own eviction but not sure if Mnesia invalidates some of those actor model assumptions

@runkelcorey runkelcorey self-assigned this Aug 6, 2026
@runkelcorey
runkelcorey marked this pull request as ready for review August 6, 2026 18:51
@runkelcorey
runkelcorey requested a review from a team as a code owner August 6, 2026 18:51
@runkelcorey
runkelcorey requested review from khansen-mbta and lemald and removed request for a team August 6, 2026 18:51
Comment on lines +116 to +118
# Get the maximum timestamp from existing data in the table.
# Uses a dynamic match spec based on the StopEvent struct to avoid brittleness
# from hardcoded field positions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: could make this a @doc block instead of just a comment.

# Parse with timestamp filtering if we have existing data
opts = if max_timestamp, do: [newer_than: max_timestamp], else: []

parser = Parse.StopEvents

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: this is only used in one place and doesn't have any possible value other than Parse.StopEvents and not any test module or anything - could the variable be removed?

Comment on lines +43 to +45
# log_parse_error returns nil, so we return nil on error to avoid passing
# invalid data to super/1. State.Server expects nil to mean
# "no data to insert" and will handle it gracefully.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I'm kind of confused by this comment because we don't actually pass nil to super below because we intercept it in the case statement and pass :ok instead.


__MODULE__
|> :mnesia.dirty_select(match_spec)
|> Enum.each(&:mnesia.dirty_delete(__MODULE__, &1))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: could anything weird possibly happen because of using dirty_delete here instead of deleting in a transaction? Could we have an API query return some of the StopEvents that are to be deleted but not others?

Comment thread apps/state/test/state/stop_event_test.exs Outdated
Comment thread apps/state/test/state/stop_event_test.exs Outdated
Co-authored-by: Eddie Maldonado <eddie@lemald.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants