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 +7 -0
- data/ADVANCED.md +136 -0
- data/BENCHMARK.md +885 -0
- data/CHANGELOG.md +163 -0
- data/LICENSE.txt +21 -0
- data/README.md +588 -0
- data/lib/generators/judge/attribute_generator.rb +149 -0
- data/lib/generators/judge/install_generator.rb +36 -0
- data/lib/generators/judge/templates/initializer.rb.tt +18 -0
- data/lib/generators/judge/templates/migration.rb.tt +9 -0
- data/lib/judge/adapter.rb +69 -0
- data/lib/judge/client.rb +253 -0
- data/lib/judge/configuration.rb +103 -0
- data/lib/judge/decision.rb +24 -0
- data/lib/judge/errors.rb +36 -0
- data/lib/judge/facade.rb +75 -0
- data/lib/judge/pool.rb +97 -0
- data/lib/judge/question/choice.rb +36 -0
- data/lib/judge/question/noul.rb +22 -0
- data/lib/judge/question/score.rb +36 -0
- data/lib/judge/question.rb +93 -0
- data/lib/judge/rails/attributes.rb +161 -0
- data/lib/judge/rails/definition.rb +156 -0
- data/lib/judge/rails/jobs.rb +156 -0
- data/lib/judge/rails/locale/en.yml +6 -0
- data/lib/judge/rails/migration.rb +115 -0
- data/lib/judge/rails/refresh.rb +37 -0
- data/lib/judge/rails/refresh_job.rb +15 -0
- data/lib/judge/rails/registry.rb +57 -0
- data/lib/judge/rails/relation.rb +132 -0
- data/lib/judge/rails/scopes.rb +124 -0
- data/lib/judge/rails/storage.rb +104 -0
- data/lib/judge/rails/validator.rb +246 -0
- data/lib/judge/rails.rb +45 -0
- data/lib/judge/result.rb +173 -0
- data/lib/judge/result_set.rb +75 -0
- data/lib/judge/version.rb +5 -0
- data/lib/judge.rb +41 -0
- data/lib/judge_rails.rb +3 -0
- metadata +126 -0
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.
|