Add RT architecture deep-dive analysis reference
This commit is contained in:
207
docs/rt-architecture-analysis.md
Normal file
207
docs/rt-architecture-analysis.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Request Tracker Architecture: Deep-Dive Technical Analysis
|
||||||
|
|
||||||
|
## Source: RT 5.0.5 codebase analysis + official docs (docs.bestpractical.com)
|
||||||
|
## Purpose: Reference for reimplementing RT's paradigm in Rust
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. SCRIPS — Event-Driven Automation Engine
|
||||||
|
|
||||||
|
### Conceptual Model
|
||||||
|
|
||||||
|
A Scrip is a rule: "When X happens, if condition Y is met, execute action Z using template W."
|
||||||
|
|
||||||
|
Four primitives compose a scrip:
|
||||||
|
- **Condition** (RT::ScripCondition): Has `ApplicableTransTypes` — comma-separated string of transaction types it matches (e.g., "Create,Correspond") or "Any". Modules like OnCreate, OnStatusChange, UserDefined.
|
||||||
|
- **Action** (RT::ScripAction): Maps via `ExecModule` to a Perl module under `RT::Action::*` (SendEmail, AutoReply, CreateTickets, etc.). Has Argument field for parameterization.
|
||||||
|
- **Template** (RT::Template): Global or queue-specific. Types: "Perl" (Text::Template) or "Simple" (variable substitution).
|
||||||
|
- **Stage**: `TransactionCreate` (fires per-transaction) or `TransactionBatch` (fires once after batched updates).
|
||||||
|
|
||||||
|
### Data Model
|
||||||
|
|
||||||
|
```
|
||||||
|
Scrips: id, Queue(0=global), Template(name), ScripCondition(id), ScripAction(id),
|
||||||
|
Description, Disabled, CustomPrepareCode, CustomCommitCode, CustomIsApplicableCode
|
||||||
|
ObjectScrips: id, Scrip(FK), Stage, ObjectId(0=global or Queue id), SortOrder
|
||||||
|
```
|
||||||
|
|
||||||
|
Key insight: The same Scrip record can be applied to multiple queues via ObjectScrips, each with different Stage and SortOrder.
|
||||||
|
|
||||||
|
### Dispatch Model — Prepare/Commit Two-Phase
|
||||||
|
|
||||||
|
**Prepare phase** (`RT::Scrips::Prepare`):
|
||||||
|
1. Load Ticket + Transaction as SystemUser (bypass ACL)
|
||||||
|
2. Find matching Scrips: global + queue-specific, filtered by Stage, matched by Condition.ApplicableTransTypes using SQL LIKE
|
||||||
|
3. Sort by SortOrder
|
||||||
|
4. For each: `IsApplicable` (no-side-effects check), then `Prepare` (builds message, determines recipients — no send), push to prepared_scrips
|
||||||
|
|
||||||
|
**Commit phase** (`RT::Scrips::Commit`): Iterate prepared_scrips in order, call `Commit` (actual side effects)
|
||||||
|
|
||||||
|
**Dry-run mode**: On Ticket Update page load, ALL scrips run in dry-run (Prepare only, no Commit) to populate the "Scrips and Recipients" preview.
|
||||||
|
|
||||||
|
**Batch mode**: With `$UseTransactionBatch`, multiple updates accumulate in `_TransactionBatch`. `RanTransactionBatch` flag prevents infinite loops. TransactionBatch-stage scrips see ALL batched transactions.
|
||||||
|
|
||||||
|
### Invariants & Edge Cases
|
||||||
|
|
||||||
|
- Prepare failures silently skip the scrip; Commit failures are NOT retried
|
||||||
|
- Template scoping: queue-specific template overrides global for the same name
|
||||||
|
- Global + queue-specific scrips both apply; their union runs sorted together
|
||||||
|
- SystemUser escalation is essential — automation must not be gated by the triggering user's ACL
|
||||||
|
- `CustomIsApplicableCode`/`CustomPrepareCode`/`CustomCommitCode` allow code injection at three points, guarded by `ExecuteCode` right
|
||||||
|
- The `Disabled` boolean on Scrips is separate from the deprecated `Disabled` stage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. TRANSACTION QUERY BUILDER
|
||||||
|
|
||||||
|
### Conceptual Model
|
||||||
|
|
||||||
|
Five-layer pipeline: UI Tree → QueryBuilder::Tree (AST) → RT::SQL parser → RT::Tickets dispatch → DBIx::SearchBuilder SQL generation.
|
||||||
|
|
||||||
|
### Transaction Model
|
||||||
|
|
||||||
|
**Everything is a Transaction**: Status changes, owner changes, comments, correspondence, CF updates, link changes, time worked — all create Transaction records.
|
||||||
|
|
||||||
|
Schema: `id, ObjectType, ObjectId, Type, Field, OldValue, NewValue, Data, Creator, Created`
|
||||||
|
|
||||||
|
Each transaction can have MIME attachments.
|
||||||
|
|
||||||
|
### Dispatch Table Architecture
|
||||||
|
|
||||||
|
`RT::Tickets::%FIELD_METADATA` maps searchable fields to type handlers:
|
||||||
|
|
||||||
|
```
|
||||||
|
Status => [STRING], Queue => [QUEUE], Owner => [WATCHERFIELD => 'Owner'],
|
||||||
|
Requestor => [WATCHERFIELD => 'Requestor'], LinkedTo => [LINK => 'To'],
|
||||||
|
DependsOn => [LINK => To => 'DependsOn'], CF => [CUSTOMFIELD => 'Ticket'],
|
||||||
|
TxnCF => [CUSTOMFIELD => 'Transaction'], Content => [TRANSCONTENT],
|
||||||
|
Lifecycle => [LIFECYCLE], Created => [DATE => 'Created']
|
||||||
|
```
|
||||||
|
|
||||||
|
Each type dispatches to a handler (STRING → `_StringLimit`, WATCHERFIELD → `_WatcherLimit`, CUSTOMFIELD → `_CustomFieldLimit`, etc.) that generates appropriate SQL JOINs.
|
||||||
|
|
||||||
|
### TicketSQL Parser
|
||||||
|
|
||||||
|
State-machine parser with states: KEYWORD, OP, VALUE, AGGREGATOR, OPEN_PAREN, CLOSE_PAREN.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Cross-field references (`Due < CF.{TargetDate}`)
|
||||||
|
- IS/IS NOT NULL
|
||||||
|
- LIKE/STARTSWITH/ENDSWITH
|
||||||
|
- SHALLOW modifier
|
||||||
|
|
||||||
|
### Key Optimizations
|
||||||
|
|
||||||
|
- **OR→IN conversion**: `(Status='new' OR Status='open' OR Status='stalled')` → `Status IN ('new','open','stalled')`
|
||||||
|
- **EntryAggregator intelligence**: String fields default to OR for =, AND for !=; dates default to OR for equality, AND for ranges
|
||||||
|
- **Join deduplication**: `_sql_aliases` hash tracks which JOINs exist, preventing duplicates
|
||||||
|
- **Post-filtering**: Deleted tickets and ACL checks applied after SQL, not in SQL
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- CF name ambiguity (same name, global vs queue-scoped) falls back to name-based query
|
||||||
|
- IPAddressRange queries decompose = into two range comparisons
|
||||||
|
- DateTime day-equality decomposes = into >= midnight AND < next midnight
|
||||||
|
- NULL handling on CFs adds extra EXISTS check on CFs.Name
|
||||||
|
- `UseSQLForACLChecks` injects ACL JOINs at SQL level
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. CUSTOM FIELDS
|
||||||
|
|
||||||
|
### Attachment Model
|
||||||
|
|
||||||
|
Two join tables:
|
||||||
|
|
||||||
|
**ObjectCustomFields**: Maps CF to container (Queue id, or 0 for global). Controls *which* CFs appear.
|
||||||
|
**ObjectCustomFieldValues**: Stores actual values on records. `CustomField(FK), ObjectType, ObjectId, Content, LargeContent, Disabled`.
|
||||||
|
|
||||||
|
LookupTypes encode the relationship: `RT::Queue-RT::Ticket` (ticket CFs), `RT::Queue-RT::Ticket-RT::Transaction` (txn CFs), `RT::User`, `RT::Queue`, `RT::Group`.
|
||||||
|
|
||||||
|
### Typing System
|
||||||
|
|
||||||
|
13 types: Select, Freeform, Text, HTML, Wikitext, Image, Binary, Combobox, Autocomplete, Date, DateTime, IPAddress, IPAddressRange.
|
||||||
|
|
||||||
|
Each has: sort_order, selection_type flag, canonicalization flag, render types, labels.
|
||||||
|
|
||||||
|
**Values system**: MaxValues (0=unlimited, 1=single, N=capped), UniqueValues, ValuesClass (external sources), CanonicalizeClass (normalization), Pattern (validation regex), BasedOn (cascading), LinkValueTo (object linking).
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
Five distinct rights: SeeCustomField, AdminCustomField, AdminCustomFieldValues, ModifyCustomField, SetInitialCustomField.
|
||||||
|
|
||||||
|
The last creates a "set-once-at-creation" pattern. Rights are contextual (per-queue for tickets).
|
||||||
|
|
||||||
|
### Queryability
|
||||||
|
|
||||||
|
Notation: `CF.{Name}`, `TxnCF.{Name}`, `QueueCF.{Name}`, `QueueID.CF.{Name}.Content`, cross-field comparison.
|
||||||
|
|
||||||
|
Compilation: `_CustomFieldDecipher` resolves name/id to CF object → `_CustomFieldJoin` performs LEFT JOINs → `_LimitCustomField` applies operator with type-aware value parsing.
|
||||||
|
|
||||||
|
Sorting: `ORDER BY CF.{Name}` joins ObjectCustomFieldValues and, for selection types, CustomFieldValues to sort by SortOrder.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- Context-aware permissions (user sees CF on Queue A but not Queue B)
|
||||||
|
- CF name collision detection and fallback
|
||||||
|
- External values sources never stored locally
|
||||||
|
- Content vs LargeContent transparent handling
|
||||||
|
- SingleValue optimization (DISTINCT joins when MaxValues=1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. LIFECYCLES
|
||||||
|
|
||||||
|
### Conceptual Model
|
||||||
|
|
||||||
|
A Lifecycle defines: valid statuses (initial/active/inactive), allowed transitions (directed graph), transition-gating rights, UI actions, defaults, and cross-lifecycle move maps.
|
||||||
|
|
||||||
|
Stored on Queue (not ticket); tickets inherit through queue.
|
||||||
|
|
||||||
|
### State Machine Semantics
|
||||||
|
|
||||||
|
**Date invariants**:
|
||||||
|
- Moving FROM initial TO active/inactive → set Started
|
||||||
|
- Moving FROM initial/active TO inactive → set Resolved
|
||||||
|
- Moving BACK from inactive → clear Resolved (critical!)
|
||||||
|
- Moving FROM initial directly TO inactive → sets BOTH Started AND Resolved
|
||||||
|
|
||||||
|
**Transition validation** (`ValidateStatusChange`):
|
||||||
|
1. Is new status valid in lifecycle?
|
||||||
|
2. Is transition allowed? (`IsTransition`)
|
||||||
|
3. Does user have the required right? (`CheckRight` with 4-level priority: exact→wildcard-from→wildcard-to→full-wildcard→fallback)
|
||||||
|
|
||||||
|
**CheckRight priority**: `'new→open'` > `'*→open'` > `'new→*'` > `'*→*'` > fallback (DeleteTicket for deleted, ModifyTicket otherwise).
|
||||||
|
|
||||||
|
Lifecycle rights REPLACE ModifyTicket, not supplement it.
|
||||||
|
|
||||||
|
### Queue Changes
|
||||||
|
|
||||||
|
Move maps are directional (`'default→orders'` vs `'orders→default'`). Maps MUST be complete — any missing status causes a hard error.
|
||||||
|
|
||||||
|
On queue change, if owner lacks OwnTicket on new queue, ticket is auto-reassigned to Nobody (safety invariant).
|
||||||
|
|
||||||
|
### Rights Model
|
||||||
|
|
||||||
|
Custom named rights are auto-created: `'* → rejected' => 'RejectTicket'` creates a RejectTicket right.
|
||||||
|
|
||||||
|
No explicit guards for behavioral hooks — handled entirely through Scrips (OnStatusChange, OnResolve conditions).
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- Action deduplication: specific `'from→to'` overrides wildcard `'*→to'`
|
||||||
|
- Lifecycle caching; no hot-reload without restart
|
||||||
|
- Disabled lifecycles: existing queues keep working, can't be selected for new queues
|
||||||
|
- Default statuses cover specific named contexts: on_create, approved, denied, reminder_on_open, reminder_on_resolve
|
||||||
|
- Batched status changes: TransactionBatch scrips see all transactions atomically
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Architectural Patterns
|
||||||
|
|
||||||
|
1. **Transaction-centric**: Everything produces a transaction → unified audit trail, unified query interface, single Scrip event stream
|
||||||
|
2. **Container-scoped config**: CFs, Scrips, Templates all follow global→queue override pattern
|
||||||
|
3. **Prepare/Commit**: Separates planning from execution (essential for dry-run, safety)
|
||||||
|
4. **Dispatch tables**: Field types → handlers, ApplicableTransTypes → conditions — config-driven, not code-driven extensibility
|
||||||
|
5. **SystemUser identity**: Automation runs elevated; reimplementation needs explicit service account concept
|
||||||
|
6. **Aggressive caching**: Lifecycle configs, CF join aliases, ACL roles, group memberships — essential at scale
|
||||||
Reference in New Issue
Block a user