judge_rails 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ae6dde89d6c7c61652d16a3fbbea1d05297130afa724ecebea1e1b2bfa0d1e3c
4
+ data.tar.gz: 401bd55f25be604c4ed91a1a90a1f58a542b935e3cbf286bee9eae8a14d98777
5
+ SHA512:
6
+ metadata.gz: 122fcc34ecb395dd85e113e90c23268c541cb93ba8abb45b55db904b6bca21a8832f45c14ff9b6b3746671bebbc2ca6a461229ac096205842af14bba800b255a
7
+ data.tar.gz: 1a3ed5e991390557f7dfb3565bd878b2a556b068172ba670c838bf920d2f95bb9b01e1a19553910931c6491b76fdc46be19d58a1ef21c39db68e44c3b04c77c4
data/ADVANCED.md ADDED
@@ -0,0 +1,136 @@
1
+ # Advanced
2
+
3
+ Everything here is shipped, tested and supported. It is not in the README because a reader meeting
4
+ three callback modes before the second example leaves, and because none of it is needed to use the
5
+ gem. Read [README.md](README.md) first.
6
+
7
+ ## Synchronous attributes
8
+
9
+ ```ruby
10
+ judge_attribute :urgency, Judge.noul("..."), sync: true
11
+ ```
12
+
13
+ The judgment is computed inline, in a `before_save`, so the value is there the moment the record is
14
+ saved and never briefly nil.
15
+
16
+ The cost is real and it is the reason this is not the default: an HTTP call inside a save holds a
17
+ pooled database connection for its whole duration. A vendor having a slow minute becomes a connection
18
+ pool exhausted, which takes down more than this feature. Use it when a nil value is worse than a slow
19
+ save, and not otherwise.
20
+
21
+ ### `on_error`
22
+
23
+ Only a synchronous attribute can honour it. Anything else has already committed by the time the call
24
+ runs, so there is nothing left to block. Declaring `on_error` on an async attribute raises at load
25
+ time rather than silently doing nothing.
26
+
27
+ ```ruby
28
+ judge_attribute :urgency, Judge.noul("..."), sync: true, on_error: :pass # default, save goes through
29
+ judge_attribute :urgency, Judge.noul("..."), sync: true, on_error: :fail # save is blocked
30
+ judge_attribute :urgency, Judge.noul("..."), sync: true, on_error: :raise # error propagates
31
+ ```
32
+
33
+ `:pass` is the default because most judgments are advisory: a ticket with no urgency score is still a
34
+ ticket. `:fail` is for the case where acting without the judgment is worse than not acting, which in
35
+ practice means moderation.
36
+
37
+ ## Conditional attributes
38
+
39
+ ```ruby
40
+ judge_attribute :spam, Judge.noul("Is this spam?"),
41
+ source: :body,
42
+ if_condition: ->(ticket) { ticket.channel == "web" }
43
+ ```
44
+
45
+ A Symbol naming a predicate method works too. Records that fail the condition are never judged and
46
+ never enqueued, so this is the cheapest filter available: it costs no call at all, unlike
47
+ `judge_filter`, which costs one per row.
48
+
49
+ The condition is evaluated at save time and again on refresh. A record that becomes eligible later
50
+ is judged then. A lambda with no argument runs against the record, like `if_condition: -> { open? }`.
51
+
52
+ A record that stops being eligible keeps its last judgment. Closing a ticket does not erase its urgency.
53
+
54
+ ## Manual attributes
55
+
56
+ ```ruby
57
+ judge_attribute :urgency, Judge.noul("..."), callbacks: false
58
+ ```
59
+
60
+ The attribute is declared, its column and scopes exist, and nothing computes it automatically. It
61
+ only moves when you ask:
62
+
63
+ ```ruby
64
+ ticket.judge_refresh # recompute in memory
65
+ ticket.judge_refresh! # recompute and store the judgment columns
66
+ ticket.judge_refresh_later # enqueue it
67
+ ```
68
+
69
+ This is the mode for a column you backfill deliberately rather than maintain continuously.
70
+
71
+ ## Backfill
72
+
73
+ ```ruby
74
+ summary = Ticket.judge_refresh_all(batch_size: 100)
75
+ summary.to_h # => {records: 250, computed: 250, skipped: 0, failed: 0, calls: 250}
76
+
77
+ Ticket.where(channel: "chat").judge_refresh_all(resume: true)
78
+ ```
79
+
80
+ Records whose judgments are already current are skipped, so an interrupted run continues where it
81
+ stopped rather than paying for everything again. `resume: true` keeps that guarantee even when you pass
82
+ `force: true`. Each record is judged with its own class's attributes, so an STI subclass gets its own
83
+ questions. A failure is counted, not raised: judgments already paid for on that record are stored, and
84
+ the rest keep their last value.
85
+
86
+ It makes one call per record. That is deliberate: several records sharing one request degrades the
87
+ judgment badly, which `BENCHMARK.md` measures. To go faster, go wider, not fuller.
88
+
89
+ ## The enqueuer
90
+
91
+ ActiveJob is optional, as long as something enqueues: without ActiveJob and without an enqueuer of your
92
+ own, the first async save raises `Judge::ConfigurationError`. The gem enqueues through one injectable
93
+ callable, so a different queue system
94
+ is a lambda:
95
+
96
+ ```ruby
97
+ Judge::Rails::Jobs.enqueuer = lambda do |payload|
98
+ MyQueue.push(model: payload.model_name, ids: payload.ids, names: payload.names)
99
+ end
100
+
101
+ Judge::Rails::Jobs.reset_enqueuer! # back to ActiveJob
102
+ ```
103
+
104
+ The payload it receives is what the job needs to do the work later: a model name, record ids, and
105
+ attribute names. Replaying it is `Judge::Rails::Jobs.perform(payload)`. That logs and drops every
106
+ failure. Pass `raise_retryable: true` to let a 429, a 5xx or a transport error reach your queue's own
107
+ retry, which is what `RefreshJob` does.
108
+
109
+ ## Concurrency
110
+
111
+ `judge_filter`, `judge_map` and `judge_sort` fan out across records. The default is 8 threads.
112
+
113
+ ```ruby
114
+ Ticket.judge_filter("...", limit: 500, concurrency: 16)
115
+ Judge.configure { |c| c.concurrency = 16 }
116
+ ```
117
+
118
+ The pool is available on its own, and it is plain Ruby with no Rails in it:
119
+
120
+ ```ruby
121
+ Judge::Pool.map(texts, concurrency: 8) { |text| Judge.ask(question, text: text) }
122
+ ```
123
+
124
+ Input order is preserved. `concurrency: 1` runs inline without creating a thread. The first error
125
+ drains the queue so no new request starts, then is re-raised once every worker has stopped, so
126
+ nothing is left running behind a raise. When the caller is interrupted, by `Rack::Timeout` for
127
+ instance, it gets control back at once: nothing new starts, and requests already in flight finish in
128
+ the background.
129
+
130
+ The gem's own workers only make HTTP calls. They run outside the Rails executor, so they cannot wait on a
131
+ lock the caller holds, and they carry the caller's log tags. When a worker exits it closes its HTTP
132
+ connections and returns any database connection your block leased.
133
+
134
+ Measured against the live API on 100 records: 1.9x at 2 threads, 3.6x at 4, 6.9x at 8, 11.7x at 16,
135
+ 18.4x at 32, with no rate limiting observed and input tokens identical at every level. Efficiency
136
+ falls as threads rise, which is why the default is 8 and not 32. Your key's quota is yours to know.