-
Notifications
You must be signed in to change notification settings - Fork 75
/
decorators.ex
1223 lines (967 loc) · 42.1 KB
/
decorators.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
if Code.ensure_loaded?(Decorator.Define) do
defmodule Nebulex.Caching.Decorators do
@moduledoc """
Declarative annotation-based caching via function
[decorators](https://github.com/arjan/decorator).
For caching declaration, the abstraction provides three Elixir function
decorators: `cacheable `, `cache_evict`, and `cache_put`, which allow
functions to trigger cache population or cache eviction.
Let us take a closer look at each annotation.
> Inspired by [Spring Cache Abstraction](https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/cache.html).
## `cacheable` decorator
As the name implies, `cacheable` is used to demarcate functions that are
cacheable - that is, functions for whom the result is stored into the cache
so, on subsequent invocations (with the same arguments), the value in the
cache is returned without having to actually execute the function. In its
simplest form, the decorator/annotation declaration requires the name of
the cache associated with the annotated function:
@decorate cacheable(cache: Cache)
def get_account(id) do
# the logic for retrieving the account ...
end
In the snippet above, the function `get_account/1` is associated with the
cache named `Cache`. Each time the function is called, the cache is checked
to see whether the invocation has been already executed and does not have
to be repeated.
### Default Key Generation
Since caches are essentially key-value stores, each invocation of a cached
function needs to be translated into a suitable key for cache access.
Out of the box, the caching abstraction uses a simple key-generator
based on the following algorithm:
* If no params are given, return `0`.
* If only one param is given, return that param as key.
* If more than one param is given, return a key computed from the hashes
of all parameters (`:erlang.phash2(args)`).
> **IMPORTANT:** Since Nebulex v2.1.0, the default key generation implements
the algorithm described above, breaking backward compatibility with older
versions. Therefore, you may need to change your code in case of using the
default key generation.
The default key generator is provided by the cache via the callback
`c:Nebulex.Cache.__default_key_generator__/0` and it is applied only
if the option `key:` or `keys:` is not configured. Defaults to
`Nebulex.Caching.SimpleKeyGenerator`. You can change the default
key generator at compile time with the option `:default_key_generator`.
For example, one can define a cache with a default key generator as:
defmodule MyApp.Cache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local,
default_key_generator: __MODULE__
@behaviour Nebulex.Caching.KeyGenerator
@impl true
def generate(mod, fun, args), do: :erlang.phash2({mod, fun, args})
end
The key generator module must implement the `Nebulex.Caching.KeyGenerator`
behaviour.
> **IMPORTANT:** There are some caveats to keep in mind when using
the key generator, therefore, it is highly recommended to review
`Nebulex.Caching.KeyGenerator` behaviour documentation before.
Also, you can provide a different key generator at any time
(overriding the default one) when using any caching annotation
through the option `:key_generator`. For example:
# With a module implementing the key-generator behaviour
@decorate cache_put(cache: Cache, key_generator: CustomKeyGenerator)
def update_account(account) do
# the logic for updating the given entity ...
end
# With the shorthand tuple {module, args}
@decorate cache_put(
cache: Cache,
key_generator: {CustomKeyGenerator, [account.name]}
)
def update_account2(account) do
# the logic for updating the given entity ...
end
# With a MFA tuple
@decorate cache_put(
cache: Cache,
key_generator: {AnotherModule, :genkey, [account.id]}
)
def update_account3(account) do
# the logic for updating the given entity ...
end
> The `:key_generator` option is available for all caching annotations.
### Custom Key Generation Declaration
Since caching is generic, it is quite likely the target functions have
various signatures that cannot be simply mapped on top of the cache
structure. This tends to become obvious when the target function has
multiple arguments out of which only some are suitable for caching
(while the rest are used only by the function logic). For example:
@decorate cacheable(cache: Cache)
def get_account(email, include_users?) do
# the logic for retrieving the account ...
end
At first glance, while the boolean argument influences the way the account
is found, it is no use for the cache.
For such cases, the `cacheable` decorator allows the user to specify the
key explicitly based on the function attributes.
@decorate cacheable(cache: Cache, key: {Account, email})
def get_account(email, include_users?) do
# the logic for retrieving the account ...
end
@decorate cacheable(cache: Cache, key: {Account, user.account_id})
def get_user_account(%User{} = user) do
# the logic for retrieving the account ...
end
It is also possible passing options to the cache, like so:
@decorate cacheable(
cache: Cache,
key: {Account, email},
opts: [ttl: 300_000]
)
def get_account(email, include_users?) do
# the logic for retrieving the account ...
end
See the **"Shared Options"** section below.
### Functions with multiple clauses
Since [decorator lib](https://github.com/arjan/decorator#functions-with-multiple-clauses)
is used, it is important to be aware of its recommendations, warns,
limitations, and so on. In this case, for functions with multiple clauses
the general advice is to create an empty function head, and call the
decorator on that head, like so:
@decorate cacheable(cache: Cache, key: email)
def get_account(email \\\\ nil)
def get_account(nil), do: nil
def get_account(email) do
# the logic for retrieving the account ...
end
## `cache_put` decorator
For cases where the cache needs to be updated without interfering with the
function execution, one can use the `cache_put` decorator. That is, the
method will always be executed and its result placed into the cache
(according to the `cache_put` options). It supports the same options as
`cacheable`.
@decorate cache_put(cache: Cache, key: {Account, acct.email})
def update_account(%Account{} = acct, attrs) do
# the logic for updating the account ...
end
Note that using `cache_put` and `cacheable` annotations on the same function
is generally discouraged because they have different behaviors. While the
latter causes the method execution to be skipped by using the cache, the
former forces the execution in order to execute a cache update. This leads
to unexpected behavior and with the exception of specific corner-cases
(such as decorators having conditions that exclude them from each other),
such declarations should be avoided.
## `cache_evict` decorator
The cache abstraction allows not just the population of a cache store but
also eviction. This process is useful for removing stale or unused data from
the cache. Opposed to `cacheable`, the decorator `cache_evict` demarcates
functions that perform cache eviction, which are functions that act as
triggers for removing data from the cache. The `cache_evict` decorator not
only allows a key to be specified, but also a set of keys. Besides, extra
options like`all_entries` which indicates whether a cache-wide eviction
needs to be performed rather than just an entry one (based on the key or
keys):
@decorate cache_evict(cache: Cache, key: {Account, email})
def delete_account_by_email(email) do
# the logic for deleting the account ...
end
@decorate cacheable(
cache: Cache,
keys: [{Account, acct.id}, {Account, acct.email}]
)
def delete_account(%Account{} = acct) do
# the logic for deleting the account ...
end
@decorate cacheable(cache: Cache, all_entries: true)
def delete_all_accounts do
# the logic for deleting all the accounts ...
end
The option `all_entries:` comes in handy when an entire cache region needs
to be cleared out - rather than evicting each entry (which would take a
long time since it is inefficient), all the entries are removed in one
operation as shown above.
## Shared Options
All three cache annotations explained previously accept the following
options:
* `:cache` - Defines what cache to use (required). Raises `ArgumentError`
if the option is not present. It can be also a MFA tuple to resolve the
cache dynamically in runtime by calling it. See "The :cache option"
section below for more information.
* `:key` - Defines the cache access key (optional). It overrides the
`:key_generator` option. If this option is not present, a default
key is generated by the configured or default key generator.
* `:opts` - Defines the cache options that will be passed as argument
to the invoked cache function (optional).
* `:match` - Match function `t:match_fun/0`. This function is for matching
and deciding whether the code-block evaluation result (which is received
as an argument) is cached or not. The function should return:
* `true` - the code-block evaluation result is cached as it is
(the default).
* `{true, value}` - `value` is cached. This is useful to set what
exactly must be cached.
* `{true, value, opts}` - `value` is cached with the options given by
`opts`. This return allows us to set the value to be cached, as well
as the runtime options for storing it (e.g.: the `ttl`).
* `false` - Nothing is cached.
The default match function looks like this:
```elixir
fn
{:error, _} -> false
:error -> false
nil -> false
_ -> true
end
```
By default, if the code-block evaluation returns any of the following
terms/values `nil`, `:error`, `{:error, term}`, the default match
function returns `false` (the returned result is not cached),
otherwise, `true` is returned (the returned result is cached).
* `:key_generator` - The custom key-generator to be used (optional).
If present, this option overrides the default key generator provided
by the cache, and it is applied only if the option `key:` or `keys:`
is not present. In other words, the option `key:` or `keys:` overrides
the `:key_generator` option. See "The `:key_generator` option" section
below for more information about the possible values.
* `:on_error` - It may be one of `:raise` (the default) or `:nothing`.
The decorators/annotations call the cache under the hood, hence,
by default, any error or exception at executing a cache command
is propagated. When this option is set to `:nothing`, any error
or exception executing a cache command is ignored and the annotated
function is executed normally.
### The `:cache` option
The cache option can be the de defined cache module or an MFA tuple to
resolve the cache dynamically in runtime. When it is an MFA tuple, the
MFA is invoked passing the calling module, function name, and arguments
by default, and the MFA arguments are passed as extra arguments.
For example:
@decorate cacheable(cache: {MyApp.Cache, :cache, []}, key: var)
def some_function(var) do
# Some logic ...
end
The annotated function above will call `MyApp.Cache.cache(mod, fun, args)`
to resolve the cache in runtime, where `mod` is the calling module, `fun`
the calling function name, and `args` the calling arguments.
Also, we can define the function passing some extra arguments, like so:
@decorate cacheable(cache: {MyApp.Cache, :cache, ["extra"]}, key: var)
def some_function(var) do
# Some logic ...
end
In this case, the MFA will be invoked by adding the extra arguments, like:
`MyApp.Cache.cache(mod, fun, args, "extra")`.
### The `:key_generator` option
The possible values for the `:key_generator` are:
* A module implementing the `Nebulex.Caching.KeyGenerator` behaviour.
* A MFA tuple `{module, function, args}` for a function to call to
generate the key before the cache is invoked. A shorthand value of
`{module, args}` is equivalent to
`{module, :generate, [calling_module, calling_function_name, args]}`.
## Putting all together
Supposing we are using `Ecto` and we want to define some cacheable functions
within the context `MyApp.Accounts`:
# The config
config :my_app, MyApp.Cache,
gc_interval: 86_400_000, #=> 1 day
backend: :shards
# The Cache
defmodule MyApp.Cache do
use Nebulex.Cache,
otp_app: :my_app,
adapter: Nebulex.Adapters.Local
end
# Some Ecto schema
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field(:username, :string)
field(:password, :string)
field(:role, :string)
end
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password, :role])
|> validate_required([:username, :password, :role])
end
end
# Accounts context
defmodule MyApp.Accounts do
use Nebulex.Caching
alias MyApp.Accounts.User
alias MyApp.{Cache, Repo}
@ttl :timer.hours(1)
@decorate cacheable(cache: Cache, key: {User, id}, opts: [ttl: @ttl])
def get_user!(id) do
Repo.get!(User, id)
end
@decorate cacheable(
cache: Cache,
key: {User, username},
opts: [ttl: @ttl]
)
def get_user_by_username(username) do
Repo.get_by(User, [username: username])
end
@decorate cache_put(
cache: Cache,
keys: [{User, usr.id}, {User, usr.username}],
match: &match_update/1
)
def update_user(%User{} = usr, attrs) do
usr
|> User.changeset(attrs)
|> Repo.update()
end
defp match_update({:ok, usr}), do: {true, usr}
defp match_update({:error, _}), do: false
@decorate cache_evict(
cache: Cache,
keys: [{User, usr.id}, {User, usr.username}]
)
def delete_user(%User{} = usr) do
Repo.delete(usr)
end
def create_user(attrs \\\\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
end
See [Cache Usage Patterns Guide](http://hexdocs.pm/nebulex/cache-usage-patterns.html).
"""
use Decorator.Define, cacheable: 1, cache_evict: 1, cache_put: 1
import Nebulex.Helpers
import Record
## Types
# Key reference spec
defrecordp(:keyref, :"$nbx_cache_keyref", cache: nil, key: nil)
@typedoc "Type spec for a key reference"
@type keyref :: record(:keyref, cache: Nebulex.Cache.t(), key: any)
@typedoc "Type for :on_error option"
@type on_error_opt :: :raise | :nothing
@typedoc "Match function type"
@type match_fun :: (any -> boolean | {true, any} | {true, any, Keyword.t()})
@typedoc "Type spec for the option :references"
@type references :: (any -> any) | nil | any
## API
@doc """
Provides a way of annotating functions to be cached (cacheable aspect).
The returned value by the code block is cached if it doesn't exist already
in cache, otherwise, it is returned directly from cache and the code block
is not executed.
## Options
* `:references` - (Optional) (`t:references/0`) Indicates the key given
by the option `:key` references another key given by the option
`:references`. In other words, when it is present, this option tells
the `cacheable` decorator to store the function's block result under
the referenced key given by the option `:references`, and the referenced
key under the key given by the option `:key`. The value could be:
* `nil` - (Default) It is ignored (no key references).
* `(term -> keyref | term)` - An anonymous function receiving the
result of the function's code block evaluation and must return the
referenced key. There is also a special type of return in case you
want to reference a key located in an external/different cache than
the one defined with the options `:key` or `:key_generator`. In this
scenario, you must return a special type `t:keyref/0`, which can be
build with the macro [`keyref/2`](`Nebulex.Caching.keyref/2`).
See the "External referenced keys" section below.
* `any` - It could be an explicit term or value, for example, a fixed
value or a function argument.
See the "Referenced keys" section for more information.
See the "Shared options" section at the module documentation.
## Examples
defmodule MyApp.Example do
use Nebulex.Caching
alias MyApp.Cache
@ttl :timer.hours(1)
@decorate cacheable(cache: Cache, key: id, opts: [ttl: @ttl])
def get_by_id(id) do
# your logic (maybe the loader to retrieve the value from the SoR)
end
@decorate cacheable(cache: Cache, key: email, references: & &1.id)
def get_by_email(email) do
# your logic (maybe the loader to retrieve the value from the SoR)
end
@decorate cacheable(cache: Cache, key: clauses, match: &match_fun/1)
def all(clauses) do
# your logic (maybe the loader to retrieve the value from the SoR)
end
defp match_fun([]), do: false
defp match_fun(_), do: true
end
The **Read-through** pattern is supported by this decorator. The loader to
retrieve the value from the system-of-record (SoR) is your function's logic
and the rest is provided by the macro under-the-hood.
## Referenced keys
Referenced keys are particularly useful when you have multiple different
keys keeping the same value. For example, let's imagine we have an schema
`User` with more than one unique field, like `:id`, `:email`, and `:token`.
We may have a module with functions retrieving the user account by any of
those fields, like so:
defmodule MyApp.UserAccounts do
use Nebulex.Caching
alias MyApp.Cache
@decorate cacheable(cache: Cache, key: id)
def get_user_account(id) do
# your logic ...
end
@decorate cacheable(cache: Cache, key: email)
def get_user_account_by_email(email) do
# your logic ...
end
@decorate cacheable(cache: Cache, key: token)
def get_user_account_by_token(token) do
# your logic ...
end
@decorate cache_evict(
cache: Cache,
keys: [user.id, user.email, user.token]
)
def update_user_account(user) do
# your logic ...
end
end
As you notice, all the three functions will end up storing the same user
record under a different key. This is not very efficient in terms of
memory space, is it? Besides, when the user record is updated, we have
to invalidate the previously cached entries, which means, we have to
specify in the `cache_evict` decorator all the different keys the user
account has ben cached under.
By means of the referenced keys, we can address it in a better and simpler
way. The module will look like this:
defmodule MyApp.UserAccounts do
use Nebulex.Caching
alias MyApp.Cache
@decorate cacheable(cache: Cache, key: id)
def get_user_account(id) do
# your logic ...
end
@decorate cacheable(cache: Cache, key: email, references: & &1.id)
def get_user_account_by_email(email) do
# your logic ...
end
@decorate cacheable(cache: Cache, key: token, references: & &1.id)
def get_user_account_by_token(token) do
# your logic ...
end
@decorate cache_evict(cache: Cache, key: user.id)
def update_user_account(user) do
# your logic ...
end
end
With the option `:references` we are indicating to the `cacheable` decorator
to store the user id (`& &1.id` - assuming the function returns an user
record) under the key `email` and the key `token`, and the user record
itself under the user id, which is the referenced key. This time, instead of
storing the same object three times, it will be stored only once under the
user id, and the other entries will just keep a reference to it. When the
functions `get_user_account_by_email/1` or `get_user_account_by_token/1`
are executed, the decorator will automatically handle it; under-the-hood,
it will fetch the referenced key given by `email` or `token` first, and
then get the user record under the referenced key.
On the other hand, in the eviction function `update_user_account/1`, since
the user record is stored only once under the user's ID, we could set the
option `:key` to the user's ID, without specifying multiple keys like in the
previous case. However, there is a caveat: _"the `cache_evict` decorator
doesn't evict the references automatically"_. See the
["CAVEATS"](#cacheable/3-caveats) section below.
### External referenced keys
Previously, we saw how to work with referenced keys but on the same cache,
like "internal references." Despite this being the typical case scenario,
there could be situations where you may want to reference a key stored in a
different or external cache. Why would I want to reference a key located in
a separate cache? There may be multiple reasons, but let's give a few
examples.
* One example is when you have a Redis cache; in such case, you likely
want to optimize the calls to Redis as much as possible. Therefore, you
should store the referenced keys in a local cache and the values in
Redis. This way, we only hit Redis to access the keys with the actual
values, and the decorator resolves the referenced keys locally.
* Another example is for keeping the cache key references isolated,
preferably locally. Then, apply a different eviction (or garbage
collection) policy for the references; one may want to expire the
references more often to avoid having dangling keys since the
`cache_evict` decorator doesn't remove the references automatically,
just the defined key (or keys). See the
["CAVEATS"](#cacheable/3-caveats) section below.
Let us modify the previous _"user accounts"_ example based on the Redis
scenario:
defmodule MyApp.UserAccounts do
use Nebulex.Caching
alias MyApp.{LocalCache, RedisCache}
@decorate cacheable(cache: RedisCache, key: id)
def get_user_account(id) do
# your logic ...
end
@decorate cacheable(
cache: LocalCache,
key: email,
references: &keyref(RedisCache, &1.id)
)
def get_user_account_by_email(email) do
# your logic ...
end
@decorate cacheable(
cache: LocalCache,
key: token,
references: &keyref(RedisCache, &1.id)
)
def get_user_account_by_token(token) do
# your logic ...
end
@decorate cache_evict(cache: RedisCache, key: user.id)
def update_user_account(user) do
# your logic ...
end
end
The functions `get_user_account/1` and `update_user_account/2` use
`RedisCache` to store the real value in Redis while
`get_user_account_by_email/1` and `get_user_account_by_token/1` use
`LocalCache` to store the referenced keys. Then, with the option
`references: &keyref(RedisCache, &1.id)` we are telling the `cacheable`
decorator the referenced key given by `&1.id` is located in the cache
`RedisCache`; underneath, the macro [`keyref/2`](`Nebulex.Caching.keyref/2`)
builds the special return type for the external cache reference.
### CAVEATS
* When the `cache_evict` decorator annotates a key (or keys) to evict, the
decorator removes only the entry associated with that key. Therefore, if
the key has references, those are not automatically removed, which means
dangling keys. However, there are multiple ways to address dangling keys
(or references):
* The first (and the simplest) sets a TTL to the reference. For example:
`cacheable(key: name, references: & &1.id, opts: [ttl: @ttl])`. You can
also specify a different TTL for the referenced key:
`references: &keyref(&1.id, ttl: @another_ttl)`.
* The second alternative, perhaps the most recommended, is having a
separate cache to keep the references (e.g., a cache using the local
adapter). This way, you could provide a different eviction or GC
configuration to run the GC more often and keep the references cache
clean. See
["External referenced keys"](#cacheable/3-external-referenced-keys).
* The third alternative uses the `:keys` option for specifying a key and
its references. For example, if you have
`@decorate cacheable(key: email, references: & &1.id)`, the eviction
may look like this `@decorate cache_evict(keys: [user.id, user.email])`.
This one is perhaps the least ideal option because it is cumbersome;
you have to know and specify the key and all its references, and at the
same time, you will need to have access to the key and references in the
arguments, which sometimes is not possible because you may receive only
the ID, but not the email.
"""
def cacheable(attrs, block, context) do
caching_action(:cacheable, attrs, block, context)
end
@doc """
Provides a way of annotating functions to be evicted; but updating the
cached key instead of deleting it.
The content of the cache is updated without interfering with the function
execution. That is, the method would always be executed and the result
cached.
The difference between `cacheable/3` and `cache_put/3` is that `cacheable/3`
will skip running the function if the key exists in the cache, whereas
`cache_put/3` will actually run the function and then put the result in
the cache.
## Options
* `:keys` - The set of cached keys to be updated with the returned value
on function completion. It overrides `:key` and `:key_generator`
options.
See the "Shared options" section at the module documentation.
## Examples
defmodule MyApp.Example do
use Nebulex.Caching
alias MyApp.Cache
@ttl :timer.hours(1)
@decorate cache_put(cache: Cache, key: id, opts: [ttl: @ttl])
def update!(id, attrs \\\\ %{}) do
# your logic (maybe write data to the SoR)
end
@decorate cache_put(
cache: Cache,
key: id,
match: &match_fun/1,
opts: [ttl: @ttl]
)
def update(id, attrs \\\\ %{}) do
# your logic (maybe write data to the SoR)
end
@decorate cache_put(
cache: Cache,
keys: [object.name, object.id],
match: &match_fun/1,
opts: [ttl: @ttl]
)
def update_object(object) do
# your logic (maybe write data to the SoR)
end
defp match_fun({:ok, updated}), do: {true, updated}
defp match_fun({:error, _}), do: false
end
The **Write-through** pattern is supported by this decorator. Your function
provides the logic to write data to the system-of-record (SoR) and the rest
is provided by the decorator under-the-hood.
"""
def cache_put(attrs, block, context) do
caching_action(:cache_put, attrs, block, context)
end
@doc """
Provides a way of annotating functions to be evicted (eviction aspect).
On function's completion, the given key or keys (depends on the `:key` and
`:keys` options) are deleted from the cache.
## Options
* `:keys` - Defines the set of keys to be evicted from cache on function
completion. It overrides `:key` and `:key_generator` options.
* `:all_entries` - Defines if all entries must be removed on function
completion. Defaults to `false`.
* `:before_invocation` - Boolean to indicate whether the eviction should
occur after (the default) or before the function executes. The former
provides the same semantics as the rest of the annotations; once the
function completes successfully, an action (in this case eviction)
on the cache is executed. If the function does not execute (as it might
be cached) or an exception is raised, the eviction does not occur.
The latter (`before_invocation: true`) causes the eviction to occur
always, before the function is invoked; this is useful in cases where
the eviction does not need to be tied to the function outcome.
See the "Shared options" section at the module documentation.
## Examples
defmodule MyApp.Example do
use Nebulex.Caching
alias MyApp.Cache
@decorate cache_evict(cache: Cache, key: id)
def delete(id) do
# your logic (maybe write/delete data to the SoR)
end
@decorate cache_evict(cache: Cache, keys: [object.name, object.id])
def delete_object(object) do
# your logic (maybe write/delete data to the SoR)
end
@decorate cache_evict(cache: Cache, all_entries: true)
def delete_all do
# your logic (maybe write/delete data to the SoR)
end
end
The **Write-through** pattern is supported by this decorator. Your function
provides the logic to write data to the system-of-record (SoR) and the rest
is provided by the decorator under-the-hood. But in contrast with `update`
decorator, when the data is written to the SoR, the key for that value is
deleted from cache instead of updated.
"""
def cache_evict(attrs, block, context) do
caching_action(:cache_evict, attrs, block, context)
end
@doc """
A convenience function for building a cache key reference when using the
`cacheable` decorator. If you want to build an external reference, which is,
referencing a `key` stored in an external cache, you have to provide the
`cache` where the `key` is located to. The `cache` argument is optional,
and by default is `nil`, which means, the referenced `key` is in the same
cache provided via `:key` or `:key_generator` options (internal reference).
**NOTE:** In case you need to build a reference, consider using the macro
`Nebulex.Caching.keyref/2` instead.
See `cacheable/3` decorator for more information about external references.
## Examples
iex> Nebulex.Caching.Decorators.build_keyref("my-key")
{:"$nbx_cache_keyref", nil, "my-key"}
iex> Nebulex.Caching.Decorators.build_keyref(MyCache, "my-key")
{:"$nbx_cache_keyref", MyCache, "my-key"}
"""
@spec build_keyref(Nebulex.Cache.t(), term) :: keyref()
def build_keyref(cache \\ nil, key) do
keyref(cache: cache, key: key)
end
## Private Functions
defp caching_action(action, attrs, block, context) do
cache = attrs[:cache] || raise ArgumentError, "expected cache: to be given as argument"
opts_var = attrs[:opts] || []
on_error_var = on_error_opt(attrs)
match_var = attrs[:match] || default_match_fun()
args =
context.args
|> Enum.reduce([], &walk/2)
|> Enum.reverse()
cache_block = cache_block(cache, args, context)
keygen_block = keygen_block(attrs, args, context)
action_block = action_block(action, block, attrs, keygen_block)
quote do
cache = unquote(cache_block)
opts = unquote(opts_var)
match = unquote(match_var)
on_error = unquote(on_error_var)
unquote(action_block)
end
end
defp default_match_fun do
quote do
fn
{:error, _} -> false
:error -> false
nil -> false
_ -> true
end
end
end
defp walk({:\\, _, [ast, _]}, acc) do
walk(ast, acc)
end
defp walk({:=, _, [_, ast]}, acc) do
walk(ast, acc)
end
defp walk({var, _meta, context} = ast, acc) when is_atom(var) and is_atom(context) do
if match?("_" <> _, "#{var}") or Macro.special_form?(var, 0) do
acc
else
[ast | acc]
end
end
defp walk(_ast, acc) do
acc
end
# MFA cache: `{module, function, args}`
defp cache_block({:{}, _, [mod, fun, cache_args]}, args, ctx) do
quote do
unquote(mod).unquote(fun)(
unquote(ctx.module),
unquote(ctx.name),
unquote(args),
unquote_splicing(cache_args)
)
end
end
# Module implementing the cache behaviour (default)
defp cache_block({_, _, _} = cache, _args, _ctx) do
quote(do: unquote(cache))
end
defp keygen_block(attrs, args, ctx) do
cond do
key = Keyword.get(attrs, :key) ->
quote(do: unquote(key))
keygen = Keyword.get(attrs, :key_generator) ->
keygen_call(keygen, ctx, args)
true ->
quote do
cache.__default_key_generator__().generate(
unquote(ctx.module),
unquote(ctx.name),
unquote(args)
)
end
end
end
# MFA key-generator: `{module, function, args}`
defp keygen_call({:{}, _, [mod, fun, keygen_args]}, _ctx, _args) do
quote do
unquote(mod).unquote(fun)(unquote_splicing(keygen_args))
end
end
# Key-generator tuple `{module, args}`, where the `module` implements
# the key-generator behaviour
defp keygen_call({{_, _, _} = mod, keygen_args}, ctx, _args) when is_list(keygen_args) do
quote do
unquote(mod).generate(unquote(ctx.module), unquote(ctx.name), unquote(keygen_args))
end
end
# Key-generator module implementing the behaviour
defp keygen_call({_, _, _} = keygen, ctx, args) do
quote do
unquote(keygen).generate(unquote(ctx.module), unquote(ctx.name), unquote(args))
end
end
defp action_block(:cacheable, block, attrs, keygen) do
references = Keyword.get(attrs, :references)
quote do
unquote(__MODULE__).eval_cacheable(
cache,
unquote(keygen),
unquote(references),
opts,
on_error,
match,
fn -> unquote(block) end
)
end
end
defp action_block(:cache_put, block, attrs, keygen) do
keys = get_keys(attrs)
key =
if is_list(keys) and length(keys) > 0,
do: {:"$keys", keys},
else: keygen
quote do
result = unquote(block)
unquote(__MODULE__).run_cmd(
unquote(__MODULE__),
:eval_match,
[result, match, cache, unquote(key), opts],
on_error,
result
)
result
end
end
defp action_block(:cache_evict, block, attrs, keygen) do
before_invocation? = attrs[:before_invocation] || false