Class: DiscordStore::Replay

Inherits:
Object
  • Object
show all
Defined in:
lib/discord_store/replay.rb

Overview

Rebuilds a local database from the Discord log.

This is the operation that decides whether any of this is a real design or a stunt. If the channel is the source of truth, then a machine holding no local file must be able to reach the same state by reading it, and it must reach the same state every time. That is the whole contract:

rm storage/production.sqlite3 && rake discord:replay

should leave the database exactly as it was, on any machine, from nothing but a bot token and a channel ID.

Statements replay inside a local transaction per journalled transaction, so a batch that was atomic when written is atomic when replayed.

Instance Method Summary collapse

Constructor Details

#initialize(connection:, journal:) ⇒ Replay

Returns a new instance of Replay.

Parameters:

  • connection (ActiveRecord::ConnectionAdapters::AbstractAdapter)
  • journal (Journal)


21
22
23
24
# File 'lib/discord_store/replay.rb', line 21

def initialize(connection:, journal:)
  @connection = connection
  @journal = journal
end

Instance Method Details

#call(from: nil, progress: nil) ⇒ Hash

Returns statements:, cursor:, skipped:.

Parameters:

  • from (String, nil) (defaults to: nil) —

    cursor to resume after; nil replays everything

  • progress (#call, nil) (defaults to: nil) —

    called with (applied, cursor) periodically

Returns:

  • (Hash) —

    statements:, cursor:, skipped:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/discord_store/replay.rb', line 29

def call(from: nil, progress: nil)
  applied = 0
  statements = 0
  skipped = 0
  cursor = from

  # Replay is a rewrite of local state from the log, so it must not be
  # journalled back into the log. Without this a replay would duplicate
  # every transaction it read.
  @journal.while_paused do
    # Inside the pause, because reading the stored cursor creates the
    # bookkeeping table, and that DDL must not become a log entry.
    from ||= stored_cursor
    cursor = from

    @journal.each_transaction(from: from) do |entry, record_cursor|
      begin
        apply(entry["tx"])
        applied += 1
        statements += entry["tx"].size
      rescue StandardError => e
        skipped += 1
        warn "discord_store: replay skipped #{record_cursor}: #{e.class}: #{e.message}"
      end

      cursor = record_cursor
      store_cursor(cursor)
      progress&.call(applied, cursor)
    end
  end

  @journal.local_cursor = cursor
  { applied: applied, statements: statements, cursor: cursor, skipped: skipped }
end

#status ⇒ Hash

How far behind the local database is.

Returns:

  • (Hash) —

    remote:, behind:



67
68
69
70
71
# File 'lib/discord_store/replay.rb', line 67

def status
  local = stored_cursor
  remote = @journal.tip
  { local: local, remote: remote, behind: local != remote }
end