Class: Pikuri::Thunderbird::Gloda

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/thunderbird/gloda.rb,
lib/pikuri/thunderbird/gloda/mail.rb,
lib/pikuri/thunderbird/gloda/contacts.rb

Overview

Provides queryable access to Thunderbird's Gloda index (+global-messages-db.sqlite+) — a pre-decoded corpus: Gloda has already MIME-decoded, HTML-stripped, and split every message into body / subject / attachment-names / author / recipients, and keeps it fresh for free. This is the service: it snapshots the locked live file, builds an ephemeral FTS5 index into the copy, tracks freshness, and hands out a current connection — but runs no queries itself. Two views do the querying:

gloda = Gloda.new(gloda_path: profile.gloda_path, cache_dir: dir)
gloda.available?                       # is the Gloda file present?
Gloda::Mail.new(gloda: gloda)          # mail search + read
Gloda::Contacts.new(gloda: gloda)      # name → address + novelty
gloda.close                            # closes the snapshot connection

A view queries through #with_fresh_db; it never caches the connection (a rebuild reopens it). pikuri never touches the locked live file — it works against a DatabaseSnapshot copy and builds its own FTS5 index into that copy (Gloda's own messagesText is an fts3 vtable behind the custom mozporter tokenizer, which the sqlite3 gem can't open).

This service is specific to Gloda's schema — the FTS5 index it builds is over Gloda's decoded columns (+c0body+ … c4recipients), so it is not a generic SQLite handle and does not serve the calendar DBs (Calendar has its own DatabaseSnapshot + backend).

Freshness & consistency (one signal)

Each #with_fresh_db first compares the live Gloda change counter against the generation the current copy+index were built from; if it moved, a fresh DatabaseSnapshot is taken and the FTS5 index rebuilt (~1 s). Between changes it's a 4-byte read and a no-op. The counter bumps on any Gloda write — including read/star/tag flips during triage — so a rebuild can fire mid-conversation (desirable: the views then see new mail). See DatabaseSnapshot.

Holds no external state beyond its own snapshot + connection, but #close is required for orderly teardown.

Sharing

P_shared_locked, and sharing beats one-per-agent: the snapshot copy and the ~1 s FTS5 rebuild are then paid once for the machine, not once each. Three contracts come with that:

  • The lock is held across the caller's block. Releasing it at the yield would hand out exactly the connection a rebuild then closes, so a rebuild parks a second agent for ~1 s — the cost it would have paid alone anyway; every other query is local SQLite.
  • A view may therefore never nest #with_fresh_db inside another (the guard is a Mutex, not a Monitor).
  • #close needs no refcount: it drops the fd, and the next #with_fresh_db reopens the same copy. A co-owner pays a rebuild, never a failure.

The cache_dir is what must stay unshared — two instances over one snapshot path cp -f under each other's open fd.

Defined Under Namespace

Classes: Contacts, Mail

Constant Summary collapse

LOGGER =
Pikuri.logger_for('Thunderbird::Gloda')

Instance Method Summary collapse

Constructor Details

#initialize(gloda_path:, cache_dir:) ⇒ Gloda

Parameters:

  • gloda_path (String)

    path to global-messages-db.sqlite.

  • cache_dir (String)

    private 0700 dir for the snapshot copy. Must be this instance's alone — see the == Sharing section.



70
71
72
73
74
75
76
# File 'lib/pikuri/thunderbird/gloda.rb', line 70

def initialize(gloda_path:, cache_dir:)
  @source = gloda_path
  @snapshot = DatabaseSnapshot.new(source: gloda_path, dir: cache_dir)
  @db = nil
  @indexed_generation = nil
  @lock = Mutex.new
end

Instance Method Details

#available?Boolean

Returns whether the Gloda index file exists (the mail / contact tools' registration gate).

Returns:

  • (Boolean)

    whether the Gloda index file exists (the mail / contact tools' registration gate).



80
# File 'lib/pikuri/thunderbird/gloda.rb', line 80

def available? = File.file?(@source)

#closevoid

This method returns an undefined value.

Close the snapshot connection (the snapshot dir itself is finalizer-reaped). Idempotent, and safe while another agent shares this instance — the next #with_fresh_db reopens.



106
107
108
# File 'lib/pikuri/thunderbird/gloda.rb', line 106

def close
  @lock.synchronize { close! }
end

#with_fresh_db {|SQLite3::Database| ... } ⇒ Object

Yield the snapshot connection, guaranteed current — a stale index is rebuilt first. The sole seam the Mail / Contacts views query through; they never cache the returned connection, because a rebuild reopens it.

gloda.with_fresh_db { |db| db.execute('SELECT count(*) FROM messages') }

The lock spans the block, so the yielded connection cannot be swapped mid-query and the block must not re-enter (see == Sharing).

Yields:

  • (SQLite3::Database)

    the fresh snapshot connection.

Returns:

  • (Object)

    the block's value.



94
95
96
97
98
99
# File 'lib/pikuri/thunderbird/gloda.rb', line 94

def with_fresh_db
  @lock.synchronize do
    ensure_fresh
    yield @db
  end
end