@@ -2,14 +2,15 @@ use std::hash::{Hash, Hasher};
2
2
use std:: sync:: Arc ;
3
3
use std:: time:: Duration ;
4
4
5
+ use moka:: notification:: RemovalCause ;
5
6
use moka:: sync:: Cache ;
6
7
use pyo3:: exceptions:: PyValueError ;
7
8
use pyo3:: prelude:: * ;
8
9
use pyo3:: pyclass:: CompareOp ;
9
10
use pyo3:: types:: { PyString , PyType } ;
10
11
11
12
#[ derive( Debug ) ]
12
- enum AnyKey {
13
+ enum KeyKind {
13
14
/// String keys are the most common. If the string is short enough,
14
15
/// we can get faster and more freedom from GIL by copying a string
15
16
/// to Rust and hashing it using `ahash` instead of calling
@@ -20,47 +21,71 @@ enum AnyKey {
20
21
ShortStr ( String ) ,
21
22
22
23
/// Other keys (even long Python strings) go this (slower) way
23
- Other ( PyObject , isize ) ,
24
+ Other { py_hash : isize } ,
25
+ }
26
+
27
+ #[ derive( Debug ) ]
28
+ struct AnyKey {
29
+ obj : PyObject ,
30
+ kind : KeyKind ,
24
31
}
25
32
26
33
impl AnyKey {
27
34
const SHORT_STR : usize = 256 ;
28
35
29
36
#[ inline]
30
37
fn new_with_gil ( obj : PyObject , py : Python ) -> PyResult < Self > {
31
- if let Ok ( s) = obj. downcast_bound :: < PyString > ( py) {
32
- if s. len ( ) ? <= Self :: SHORT_STR {
33
- return Ok ( AnyKey :: ShortStr ( s. to_string ( ) ) ) ;
38
+ let kind = match obj. downcast_bound :: < PyString > ( py) {
39
+ Ok ( s) if s. len ( ) ? <= Self :: SHORT_STR => KeyKind :: ShortStr ( s. to_string ( ) ) ,
40
+ _ => {
41
+ let py_hash = obj. to_object ( py) . into_bound ( py) . hash ( ) ?;
42
+ KeyKind :: Other { py_hash }
34
43
}
35
- }
36
- let hash = obj. to_object ( py) . into_bound ( py) . hash ( ) ?;
37
- Ok ( AnyKey :: Other ( obj, hash) )
44
+ } ;
45
+ Ok ( AnyKey { obj, kind } )
38
46
}
39
47
}
40
48
41
49
impl PartialEq for AnyKey {
42
50
#[ inline]
43
51
fn eq ( & self , other : & Self ) -> bool {
44
52
match ( self , other) {
45
- ( AnyKey :: ShortStr ( lhs) , AnyKey :: ShortStr ( rhs) ) => lhs == rhs,
53
+ (
54
+ AnyKey {
55
+ kind : KeyKind :: ShortStr ( lhs) ,
56
+ ..
57
+ } ,
58
+ AnyKey {
59
+ kind : KeyKind :: ShortStr ( rhs) ,
60
+ ..
61
+ } ,
62
+ ) => lhs == rhs,
46
63
47
64
// It is expected that `hash` will be stable for an object. Hence, since we already
48
65
// know both objects' hashes, we can claim that if their hashes are different,
49
66
// the objects aren't equal. Only if the hashes are the same, the objects
50
67
// might be equal, and only in that case we raise the GIL to run Python
51
68
// rich comparison.
52
- ( AnyKey :: Other ( lhs, lhs_hash) , AnyKey :: Other ( rhs, rhs_hash) ) => {
69
+ (
70
+ AnyKey {
71
+ kind : KeyKind :: Other { py_hash : lhs_hash } ,
72
+ obj : lhs_obj,
73
+ } ,
74
+ AnyKey {
75
+ kind : KeyKind :: Other { py_hash : rhs_hash } ,
76
+ obj : rhs_obj,
77
+ } ,
78
+ ) => {
53
79
* lhs_hash == * rhs_hash
54
80
&& Python :: with_gil ( |py| {
55
- let lhs = lhs . to_object ( py) . into_bound ( py) ;
56
- let rhs = rhs . to_object ( py) . into_bound ( py) ;
81
+ let lhs = lhs_obj . to_object ( py) . into_bound ( py) ;
82
+ let rhs = rhs_obj . to_object ( py) . into_bound ( py) ;
57
83
match lhs. rich_compare ( rhs, CompareOp :: Eq ) {
58
84
Ok ( v) => v. is_truthy ( ) . unwrap_or_default ( ) ,
59
85
Err ( _) => false ,
60
86
}
61
87
} )
62
88
}
63
-
64
89
_ => false ,
65
90
}
66
91
}
@@ -70,39 +95,67 @@ impl Eq for AnyKey {}
70
95
impl Hash for AnyKey {
71
96
#[ inline]
72
97
fn hash < H : Hasher > ( & self , state : & mut H ) {
73
- match self {
74
- AnyKey :: ShortStr ( s) => s. hash ( state) ,
75
- AnyKey :: Other ( _ , hash ) => hash . hash ( state) ,
98
+ match & self . kind {
99
+ KeyKind :: ShortStr ( s) => s. hash ( state) ,
100
+ KeyKind :: Other { py_hash } => py_hash . hash ( state) ,
76
101
}
77
102
}
78
103
}
79
104
105
+ #[ inline]
106
+ fn cause_to_str ( cause : RemovalCause ) -> & ' static str {
107
+ match cause {
108
+ RemovalCause :: Expired => "expired" ,
109
+ RemovalCause :: Explicit => "explicit" ,
110
+ RemovalCause :: Replaced => "replaced" ,
111
+ RemovalCause :: Size => "size" ,
112
+ }
113
+ }
114
+
80
115
#[ pyclass]
81
116
struct Moka ( Arc < Cache < AnyKey , Arc < PyObject > , ahash:: RandomState > > ) ;
82
117
83
118
#[ pymethods]
84
119
impl Moka {
85
120
#[ new]
86
- #[ pyo3( signature = ( capacity, ttl=None , tti=None ) ) ]
87
- fn new ( capacity : u64 , ttl : Option < f64 > , tti : Option < f64 > ) -> PyResult < Self > {
121
+ #[ pyo3( signature = ( capacity, ttl=None , tti=None , eviction_listener=None ) ) ]
122
+ fn new (
123
+ capacity : u64 ,
124
+ ttl : Option < f64 > ,
125
+ tti : Option < f64 > ,
126
+ eviction_listener : Option < PyObject > ,
127
+ ) -> PyResult < Self > {
88
128
let mut builder = Cache :: builder ( ) . max_capacity ( capacity) ;
89
129
90
130
if let Some ( ttl) = ttl {
91
- let ttl_micros = ( ttl * 1000_000 .0) as u64 ;
131
+ let ttl_micros = ( ttl * 1_000_000 .0) as u64 ;
92
132
if ttl_micros == 0 {
93
133
return Err ( PyValueError :: new_err ( "ttl must be positive" ) ) ;
94
134
}
95
135
builder = builder. time_to_live ( Duration :: from_micros ( ttl_micros) ) ;
96
136
}
97
137
98
138
if let Some ( tti) = tti {
99
- let tti_micros = ( tti * 1000_000 .0) as u64 ;
139
+ let tti_micros = ( tti * 1_000_000 .0) as u64 ;
100
140
if tti_micros == 0 {
101
141
return Err ( PyValueError :: new_err ( "tti must be positive" ) ) ;
102
142
}
103
143
builder = builder. time_to_idle ( Duration :: from_micros ( tti_micros) ) ;
104
144
}
105
145
146
+ if let Some ( listener) = eviction_listener {
147
+ let listen_fn = move |k : Arc < AnyKey > , v : Arc < PyObject > , cause : RemovalCause | {
148
+ Python :: with_gil ( |py| {
149
+ let key = k. as_ref ( ) . obj . clone_ref ( py) ;
150
+ let value = v. as_ref ( ) . clone_ref ( py) ;
151
+ if let Err ( e) = listener. call1 ( py, ( key, value, cause_to_str ( cause) ) ) {
152
+ e. restore ( py)
153
+ }
154
+ } ) ;
155
+ } ;
156
+ builder = builder. eviction_listener ( Box :: new ( listen_fn) ) ;
157
+ }
158
+
106
159
Ok ( Moka ( Arc :: new (
107
160
builder. build_with_hasher ( ahash:: RandomState :: default ( ) ) ,
108
161
) ) )
0 commit comments