Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
ruanhaishen
redis
Commits
2968d8e3
Unverified
Commit
2968d8e3
authored
Dec 12, 2019
by
Salvatore Sanfilippo
Committed by
GitHub
Dec 12, 2019
Browse files
Merge branch 'unstable' into ModuleSecurity
parents
ff682d79
c6fb9d09
Changes
170
Show whitespace changes
Inline
Side-by-side
src/defrag.c
View file @
2968d8e3
...
...
@@ -374,7 +374,7 @@ long activeDefragSdsListAndDict(list *l, dict *d, int dict_val_type) {
if
((
newele
=
activeDefragStringOb
(
ele
,
&
defragged
)))
de
->
v
.
val
=
newele
,
defragged
++
;
}
else
if
(
dict_val_type
==
DEFRAG_SDS_DICT_VAL_VOID_PTR
)
{
void
*
newptr
,
*
ptr
=
ln
->
value
;
void
*
newptr
,
*
ptr
=
dictGetVal
(
de
)
;
if
((
newptr
=
activeDefragAlloc
(
ptr
)))
ln
->
value
=
newptr
,
defragged
++
;
}
...
...
@@ -919,10 +919,12 @@ int defragLaterItem(dictEntry *de, unsigned long *cursor, long long endtime) {
return
0
;
}
/* static variables serving defragLaterStep to continue scanning a key from were we stopped last time. */
static
sds
defrag_later_current_key
=
NULL
;
static
unsigned
long
defrag_later_cursor
=
0
;
/* returns 0 if no more work needs to be been done, and 1 if time is up and more work is needed. */
int
defragLaterStep
(
redisDb
*
db
,
long
long
endtime
)
{
static
sds
current_key
=
NULL
;
static
unsigned
long
cursor
=
0
;
unsigned
int
iterations
=
0
;
unsigned
long
long
prev_defragged
=
server
.
stat_active_defrag_hits
;
unsigned
long
long
prev_scanned
=
server
.
stat_active_defrag_scanned
;
...
...
@@ -930,16 +932,15 @@ int defragLaterStep(redisDb *db, long long endtime) {
do
{
/* if we're not continuing a scan from the last call or loop, start a new one */
if
(
!
cursor
)
{
if
(
!
defrag_later_
cursor
)
{
listNode
*
head
=
listFirst
(
db
->
defrag_later
);
/* Move on to next key */
if
(
current_key
)
{
serverAssert
(
current_key
==
head
->
value
);
sdsfree
(
head
->
value
);
if
(
defrag_later_current_key
)
{
serverAssert
(
defrag_later_current_key
==
head
->
value
);
listDelNode
(
db
->
defrag_later
,
head
);
cursor
=
0
;
current_key
=
NULL
;
defrag_later_
cursor
=
0
;
defrag_later_
current_key
=
NULL
;
}
/* stop if we reached the last one. */
...
...
@@ -948,21 +949,21 @@ int defragLaterStep(redisDb *db, long long endtime) {
return
0
;
/* start a new key */
current_key
=
head
->
value
;
cursor
=
0
;
defrag_later_
current_key
=
head
->
value
;
defrag_later_
cursor
=
0
;
}
/* each time we enter this function we need to fetch the key from the dict again (if it still exists) */
dictEntry
*
de
=
dictFind
(
db
->
dict
,
current_key
);
dictEntry
*
de
=
dictFind
(
db
->
dict
,
defrag_later_
current_key
);
key_defragged
=
server
.
stat_active_defrag_hits
;
do
{
int
quit
=
0
;
if
(
defragLaterItem
(
de
,
&
cursor
,
endtime
))
if
(
defragLaterItem
(
de
,
&
defrag_later_
cursor
,
endtime
))
quit
=
1
;
/* time is up, we didn't finish all the work */
/* Don't start a new BIG key in this loop, this is because the
* next key can be a list, and scanLaterList must be done in once cycle */
if
(
!
cursor
)
if
(
!
defrag_later_
cursor
)
quit
=
1
;
/* Once in 16 scan iterations, 512 pointer reallocations, or 64 fields
...
...
@@ -982,7 +983,7 @@ int defragLaterStep(redisDb *db, long long endtime) {
prev_defragged
=
server
.
stat_active_defrag_hits
;
prev_scanned
=
server
.
stat_active_defrag_scanned
;
}
}
while
(
cursor
);
}
while
(
defrag_later_
cursor
);
if
(
key_defragged
!=
server
.
stat_active_defrag_hits
)
server
.
stat_active_defrag_key_hits
++
;
else
...
...
@@ -1039,7 +1040,22 @@ void activeDefragCycle(void) {
mstime_t
latency
;
int
quit
=
0
;
if
(
server
.
aof_child_pid
!=-
1
||
server
.
rdb_child_pid
!=-
1
)
if
(
!
server
.
active_defrag_enabled
)
{
if
(
server
.
active_defrag_running
)
{
/* if active defrag was disabled mid-run, start from fresh next time. */
server
.
active_defrag_running
=
0
;
if
(
db
)
listEmpty
(
db
->
defrag_later
);
defrag_later_current_key
=
NULL
;
defrag_later_cursor
=
0
;
current_db
=
-
1
;
cursor
=
0
;
db
=
NULL
;
}
return
;
}
if
(
hasActiveChildProcess
())
return
;
/* Defragging memory while there's a fork will just do damage. */
/* Once a second, check if we the fragmentation justfies starting a scan
...
...
src/evict.c
View file @
2968d8e3
...
...
@@ -444,6 +444,7 @@ int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *lev
* Otehrwise if we are over the memory limit, but not enough memory
* was freed to return back under the limit, the function returns C_ERR. */
int
freeMemoryIfNeeded
(
void
)
{
int
keys_freed
=
0
;
/* By default replicas should ignore maxmemory
* and just be masters exact copies. */
if
(
server
.
masterhost
&&
server
.
repl_slave_ignore_maxmemory
)
return
C_OK
;
...
...
@@ -467,7 +468,7 @@ int freeMemoryIfNeeded(void) {
latencyStartMonitor
(
latency
);
while
(
mem_freed
<
mem_tofree
)
{
int
j
,
k
,
i
,
keys_freed
=
0
;
int
j
,
k
,
i
;
static
unsigned
int
next_db
=
0
;
sds
bestkey
=
NULL
;
int
bestdbid
;
...
...
@@ -598,9 +599,7 @@ int freeMemoryIfNeeded(void) {
mem_freed
=
mem_tofree
;
}
}
}
if
(
!
keys_freed
)
{
}
else
{
latencyEndMonitor
(
latency
);
latencyAddSampleIfNeeded
(
"eviction-cycle"
,
latency
);
goto
cant_free
;
/* nothing to free... */
...
...
src/expire.c
View file @
2968d8e3
...
...
@@ -64,7 +64,7 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
dbSyncDelete
(
db
,
keyobj
);
notifyKeyspaceEvent
(
NOTIFY_EXPIRED
,
"expired"
,
keyobj
,
db
->
id
);
if
(
server
.
tracking_clients
)
trackingInvalidateKey
(
keyobj
);
trackingInvalidateKey
(
keyobj
);
decrRefCount
(
keyobj
);
server
.
stat_expiredkeys
++
;
return
1
;
...
...
@@ -78,24 +78,63 @@ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
* it will get more aggressive to avoid that too much memory is used by
* keys that can be removed from the keyspace.
*
* No more than CRON_DBS_PER_CALL databases are tested at every
* iteration.
* Every expire cycle tests multiple databases: the next call will start
* again from the next db, with the exception of exists for time limit: in that
* case we restart again from the last database we were processing. Anyway
* no more than CRON_DBS_PER_CALL databases are tested at every iteration.
*
* This kind of call is used when Redis detects that timelimit_exit is
* true, so there is more work to do, and we do it more incrementally from
* the beforeSleep() function of the event loop.
* The function can perform more or less work, depending on the "type"
* argument. It can execute a "fast cycle" or a "slow cycle". The slow
* cycle is the main way we collect expired cycles: this happens with
* the "server.hz" frequency (usually 10 hertz).
*
* Expire cycle type:
* However the slow cycle can exit for timeout, since it used too much time.
* For this reason the function is also invoked to perform a fast cycle
* at every event loop cycle, in the beforeSleep() function. The fast cycle
* will try to perform less work, but will do it much more often.
*
* The following are the details of the two expire cycles and their stop
* conditions:
*
* If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
* "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
* microseconds, and is not repeated again before the same amount of time.
* The cycle will also refuse to run at all if the latest slow cycle did not
* terminate because of a time limit condition.
*
* If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
* executed, where the time limit is a percentage of the REDIS_HZ period
* as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. */
* as specified by the ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC define. In the
* fast cycle, the check of every database is interrupted once the number
* of already expired keys in the database is estimated to be lower than
* a given percentage, in order to avoid doing too much work to gain too
* little memory.
*
* The configured expire "effort" will modify the baseline parameters in
* order to do more work in both the fast and slow expire cycles.
*/
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20
/* Keys for each DB loop. */
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000
/* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25
/* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10
/* % of stale keys after which
we do extra efforts. */
void
activeExpireCycle
(
int
type
)
{
/* Adjust the running parameters according to the configured expire
* effort. The default effort is 1, and the maximum configurable effort
* is 10. */
unsigned
long
effort
=
server
.
active_expire_effort
-
1
,
/* Rescale from 0 to 9. */
config_keys_per_loop
=
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP
+
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP
/
4
*
effort
,
config_cycle_fast_duration
=
ACTIVE_EXPIRE_CYCLE_FAST_DURATION
+
ACTIVE_EXPIRE_CYCLE_FAST_DURATION
/
4
*
effort
,
config_cycle_slow_time_perc
=
ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC
+
2
*
effort
,
config_cycle_acceptable_stale
=
ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE
-
effort
;
/* This function has some global state in order to continue the work
* incrementally across calls. */
static
unsigned
int
current_db
=
0
;
/* Last DB tested. */
...
...
@@ -113,10 +152,16 @@ void activeExpireCycle(int type) {
if
(
type
==
ACTIVE_EXPIRE_CYCLE_FAST
)
{
/* Don't start a fast cycle if the previous cycle did not exit
* for time limit. Also don't repeat a fast cycle for the same period
* for time limit, unless the percentage of estimated stale keys is
* too high. Also never repeat a fast cycle for the same period
* as the fast cycle total duration itself. */
if
(
!
timelimit_exit
)
return
;
if
(
start
<
last_fast_cycle
+
ACTIVE_EXPIRE_CYCLE_FAST_DURATION
*
2
)
return
;
if
(
!
timelimit_exit
&&
server
.
stat_expired_stale_perc
<
config_cycle_acceptable_stale
)
return
;
if
(
start
<
last_fast_cycle
+
(
long
long
)
config_cycle_fast_duration
*
2
)
return
;
last_fast_cycle
=
start
;
}
...
...
@@ -130,16 +175,16 @@ void activeExpireCycle(int type) {
if
(
dbs_per_call
>
server
.
dbnum
||
timelimit_exit
)
dbs_per_call
=
server
.
dbnum
;
/* We can use at max
ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC
percentage of CPU
time
* per iteration. Since this function gets called with a frequency of
/* We can use at max
'config_cycle_slow_time_perc'
percentage of CPU
*
time
per iteration. Since this function gets called with a frequency of
* server.hz times per second, the following is the max amount of
* microseconds we can spend in this function. */
timelimit
=
1000000
*
ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC
/
server
.
hz
/
100
;
timelimit
=
config_cycle_slow_time_perc
*
1000000
/
server
.
hz
/
100
;
timelimit_exit
=
0
;
if
(
timelimit
<=
0
)
timelimit
=
1
;
if
(
type
==
ACTIVE_EXPIRE_CYCLE_FAST
)
timelimit
=
ACTIVE_EXPIRE_CYCLE_FAST_DURATION
;
/* in microseconds. */
timelimit
=
config_cycle_fast_duration
;
/* in microseconds. */
/* Accumulate some global stats as we expire keys, to have some idea
* about the number of keys that are already logically expired, but still
...
...
@@ -148,7 +193,9 @@ void activeExpireCycle(int type) {
long
total_expired
=
0
;
for
(
j
=
0
;
j
<
dbs_per_call
&&
timelimit_exit
==
0
;
j
++
)
{
int
expired
;
/* Expired and checked in a single loop. */
unsigned
long
expired
,
sampled
;
redisDb
*
db
=
server
.
db
+
(
current_db
%
server
.
dbnum
);
/* Increment the DB now so we are sure if we run out of time
...
...
@@ -172,8 +219,8 @@ void activeExpireCycle(int type) {
slots
=
dictSlots
(
db
->
expires
);
now
=
mstime
();
/* When there are less than 1% filled slots
getting random
*
keys
is expensive, so stop here waiting for better times...
/* When there are less than 1% filled slots
, sampling the key
*
space
is expensive, so stop here waiting for better times...
* The dictionary will be resized asap. */
if
(
num
&&
slots
>
DICT_HT_INITIAL_SIZE
&&
(
num
*
100
/
slots
<
1
))
break
;
...
...
@@ -181,27 +228,58 @@ void activeExpireCycle(int type) {
/* The main collection cycle. Sample random keys among keys
* with an expire set, checking for expired ones. */
expired
=
0
;
sampled
=
0
;
ttl_sum
=
0
;
ttl_samples
=
0
;
if
(
num
>
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
)
num
=
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
;
if
(
num
>
config_keys_per_loop
)
num
=
config_keys_per_loop
;
while
(
num
--
)
{
dictEntry
*
de
;
/* Here we access the low level representation of the hash table
* for speed concerns: this makes this code coupled with dict.c,
* but it hardly changed in ten years.
*
* Note that certain places of the hash table may be empty,
* so we want also a stop condition about the number of
* buckets that we scanned. However scanning for free buckets
* is very fast: we are in the cache line scanning a sequential
* array of NULL pointers, so we can scan a lot more buckets
* than keys in the same time. */
long
max_buckets
=
num
*
20
;
long
checked_buckets
=
0
;
while
(
sampled
<
num
&&
checked_buckets
<
max_buckets
)
{
for
(
int
table
=
0
;
table
<
2
;
table
++
)
{
if
(
table
==
1
&&
!
dictIsRehashing
(
db
->
expires
))
break
;
unsigned
long
idx
=
db
->
expires_cursor
;
idx
&=
db
->
expires
->
ht
[
table
].
sizemask
;
dictEntry
*
de
=
db
->
expires
->
ht
[
table
].
table
[
idx
];
long
long
ttl
;
if
((
de
=
dictGetRandomKey
(
db
->
expires
))
==
NULL
)
break
;
ttl
=
dictGetSignedIntegerVal
(
de
)
-
now
;
if
(
activeExpireCycleTryExpire
(
db
,
de
,
now
))
expired
++
;
/* Scan the current bucket of the current table. */
checked_buckets
++
;
while
(
de
)
{
/* Get the next entry now since this entry may get
* deleted. */
dictEntry
*
e
=
de
;
de
=
de
->
next
;
ttl
=
dictGetSignedIntegerVal
(
e
)
-
now
;
if
(
activeExpireCycleTryExpire
(
db
,
e
,
now
))
expired
++
;
if
(
ttl
>
0
)
{
/* We want the average TTL of keys yet not expired. */
/* We want the average TTL of keys yet
* not expired. */
ttl_sum
+=
ttl
;
ttl_samples
++
;
}
total_sampled
++
;
sampled
++
;
}
}
db
->
expires_cursor
++
;
}
total_expired
+=
expired
;
total_sampled
+=
sampled
;
/* Update the average TTL stats for this database. */
if
(
ttl_samples
)
{
...
...
@@ -225,12 +303,14 @@ void activeExpireCycle(int type) {
break
;
}
}
/* We don't repeat the cycle if there are less than 25% of keys
* found expired in the current DB. */
}
while
(
expired
>
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
/
4
);
/* We don't repeat the cycle for the current database if there are
* an acceptable amount of stale keys (logically expired but yet
* not reclained). */
}
while
((
expired
*
100
/
sampled
)
>
config_cycle_acceptable_stale
);
}
elapsed
=
ustime
()
-
start
;
server
.
stat_expire_cycle_time_used
+=
elapsed
;
latencyAddSampleIfNeeded
(
"expire-cycle"
,
elapsed
/
1000
);
/* Update our estimate of keys existing but yet to be expired.
...
...
src/geo.c
View file @
2968d8e3
...
...
@@ -466,7 +466,7 @@ void georadiusGeneric(client *c, int flags) {
/* Look up the requested zset */
robj
*
zobj
=
NULL
;
if
((
zobj
=
lookupKeyReadOrReply
(
c
,
key
,
shared
.
null
[
c
->
resp
]
))
==
NULL
||
if
((
zobj
=
lookupKeyReadOrReply
(
c
,
key
,
shared
.
emptyarray
))
==
NULL
||
checkType
(
c
,
zobj
,
OBJ_ZSET
))
{
return
;
}
...
...
@@ -566,7 +566,7 @@ void georadiusGeneric(client *c, int flags) {
/* If no matching results, the user gets an empty reply. */
if
(
ga
->
used
==
0
&&
storekey
==
NULL
)
{
addReply
Null
(
c
);
addReply
(
c
,
shared
.
emptyarray
);
geoArrayFree
(
ga
);
return
;
}
...
...
@@ -734,14 +734,14 @@ void geohashCommand(client *c) {
r
[
1
].
max
=
90
;
geohashEncode
(
&
r
[
0
],
&
r
[
1
],
xy
[
0
],
xy
[
1
],
26
,
&
hash
);
char
buf
[
1
2
];
char
buf
[
1
1
];
int
i
;
for
(
i
=
0
;
i
<
1
1
;
i
++
)
{
for
(
i
=
0
;
i
<
1
0
;
i
++
)
{
int
idx
=
(
hash
.
bits
>>
(
52
-
((
i
+
1
)
*
5
)))
&
0x1f
;
buf
[
i
]
=
geoalphabet
[
idx
];
}
buf
[
1
1
]
=
'\0'
;
addReplyBulkCBuffer
(
c
,
buf
,
1
1
);
buf
[
1
0
]
=
'\0'
;
addReplyBulkCBuffer
(
c
,
buf
,
1
0
);
}
}
}
...
...
src/hyperloglog.c
View file @
2968d8e3
...
...
@@ -700,7 +700,7 @@ int hllSparseSet(robj *o, long index, uint8_t count) {
p
+=
oplen
;
first
+=
span
;
}
if
(
span
==
0
)
return
-
1
;
/* Invalid format. */
if
(
span
==
0
||
p
>=
end
)
return
-
1
;
/* Invalid format. */
next
=
HLL_SPARSE_IS_XZERO
(
p
)
?
p
+
2
:
p
+
1
;
if
(
next
>=
end
)
next
=
NULL
;
...
...
@@ -1242,7 +1242,7 @@ void pfcountCommand(client *c) {
if
(
o
==
NULL
)
continue
;
/* Assume empty HLL for non existing var.*/
if
(
isHLLObjectOrReply
(
c
,
o
)
!=
C_OK
)
return
;
/* Merge with this HLL with our 'max' H
H
L by setting max[i]
/* Merge with this HLL with our 'max' H
L
L by setting max[i]
* to MAX(max[i],hll[i]). */
if
(
hllMerge
(
registers
,
o
)
==
C_ERR
)
{
addReplySds
(
c
,
sdsnew
(
invalid_hll_err
));
...
...
@@ -1329,7 +1329,7 @@ void pfmergeCommand(client *c) {
hdr
=
o
->
ptr
;
if
(
hdr
->
encoding
==
HLL_DENSE
)
use_dense
=
1
;
/* Merge with this HLL with our 'max' H
H
L by setting max[i]
/* Merge with this HLL with our 'max' H
L
L by setting max[i]
* to MAX(max[i],hll[i]). */
if
(
hllMerge
(
max
,
o
)
==
C_ERR
)
{
addReplySds
(
c
,
sdsnew
(
invalid_hll_err
));
...
...
src/latency.c
View file @
2968d8e3
...
...
@@ -95,7 +95,7 @@ void latencyMonitorInit(void) {
* This function is usually called via latencyAddSampleIfNeeded(), that
* is a macro that only adds the sample if the latency is higher than
* server.latency_monitor_threshold. */
void
latencyAddSample
(
char
*
event
,
mstime_t
latency
)
{
void
latencyAddSample
(
const
char
*
event
,
mstime_t
latency
)
{
struct
latencyTimeSeries
*
ts
=
dictFetchValue
(
server
.
latency_events
,
event
);
time_t
now
=
time
(
NULL
);
int
prev
;
...
...
@@ -599,7 +599,7 @@ NULL
event
=
dictGetKey
(
de
);
graph
=
latencyCommandGenSparkeline
(
event
,
ts
);
addReply
BulkCString
(
c
,
graph
);
addReply
Verbatim
(
c
,
graph
,
sdslen
(
graph
),
"txt"
);
sdsfree
(
graph
);
}
else
if
(
!
strcasecmp
(
c
->
argv
[
1
]
->
ptr
,
"latest"
)
&&
c
->
argc
==
2
)
{
/* LATENCY LATEST */
...
...
@@ -608,7 +608,7 @@ NULL
/* LATENCY DOCTOR */
sds
report
=
createLatencyReport
();
addReply
BulkCBuffer
(
c
,
report
,
sdslen
(
report
));
addReply
Verbatim
(
c
,
report
,
sdslen
(
report
)
,
"txt"
);
sdsfree
(
report
);
}
else
if
(
!
strcasecmp
(
c
->
argv
[
1
]
->
ptr
,
"reset"
)
&&
c
->
argc
>=
2
)
{
/* LATENCY RESET */
...
...
src/latency.h
View file @
2968d8e3
...
...
@@ -62,7 +62,7 @@ struct latencyStats {
};
void
latencyMonitorInit
(
void
);
void
latencyAddSample
(
char
*
event
,
mstime_t
latency
);
void
latencyAddSample
(
const
char
*
event
,
mstime_t
latency
);
int
THPIsEnabled
(
void
);
/* Latency monitoring macros. */
...
...
src/localtime.c
View file @
2968d8e3
...
...
@@ -52,8 +52,8 @@
static
int
is_leap_year
(
time_t
year
)
{
if
(
year
%
4
)
return
0
;
/* A year not divisible by 4 is not leap. */
else
if
(
year
%
100
)
return
1
;
/* If div by 4 and not 100 is surely leap. */
else
if
(
year
%
400
)
return
0
;
/* If div by 100 *and* 400 is not leap. */
else
return
1
;
/* If div by 100 and
not by
400 is leap. */
else
if
(
year
%
400
)
return
0
;
/* If div by 100 *and*
not by
400 is not leap. */
else
return
1
;
/* If div by 100 and 400 is leap. */
}
void
nolocks_localtime
(
struct
tm
*
tmp
,
time_t
t
,
time_t
tz
,
int
dst
)
{
...
...
src/lolwut.c
View file @
2968d8e3
...
...
@@ -34,8 +34,11 @@
*/
#include "server.h"
#include "lolwut.h"
#include <math.h>
void
lolwut5Command
(
client
*
c
);
void
lolwut6Command
(
client
*
c
);
/* The default target for LOLWUT if no matching version was found.
* This is what unstable versions of Redis will display. */
...
...
@@ -43,14 +46,143 @@ void lolwutUnstableCommand(client *c) {
sds
rendered
=
sdsnew
(
"Redis ver. "
);
rendered
=
sdscat
(
rendered
,
REDIS_VERSION
);
rendered
=
sdscatlen
(
rendered
,
"
\n
"
,
1
);
addReplyBulkSds
(
c
,
rendered
);
addReplyVerbatim
(
c
,
rendered
,
sdslen
(
rendered
),
"txt"
);
sdsfree
(
rendered
);
}
/* LOLWUT [VERSION <version>] [... version specific arguments ...] */
void
lolwutCommand
(
client
*
c
)
{
char
*
v
=
REDIS_VERSION
;
if
((
v
[
0
]
==
'5'
&&
v
[
1
]
==
'.'
)
||
char
verstr
[
64
];
if
(
c
->
argc
>=
3
&&
!
strcasecmp
(
c
->
argv
[
1
]
->
ptr
,
"version"
))
{
long
ver
;
if
(
getLongFromObjectOrReply
(
c
,
c
->
argv
[
2
],
&
ver
,
NULL
)
!=
C_OK
)
return
;
snprintf
(
verstr
,
sizeof
(
verstr
),
"%u.0.0"
,(
unsigned
int
)
ver
);
v
=
verstr
;
/* Adjust argv/argc to filter the "VERSION ..." option, since the
* specific LOLWUT version implementations don't know about it
* and expect their arguments. */
c
->
argv
+=
2
;
c
->
argc
-=
2
;
}
if
((
v
[
0
]
==
'5'
&&
v
[
1
]
==
'.'
&&
v
[
2
]
!=
'9'
)
||
(
v
[
0
]
==
'4'
&&
v
[
1
]
==
'.'
&&
v
[
2
]
==
'9'
))
lolwut5Command
(
c
);
else
if
((
v
[
0
]
==
'6'
&&
v
[
1
]
==
'.'
&&
v
[
2
]
!=
'9'
)
||
(
v
[
0
]
==
'5'
&&
v
[
1
]
==
'.'
&&
v
[
2
]
==
'9'
))
lolwut6Command
(
c
);
else
lolwutUnstableCommand
(
c
);
/* Fix back argc/argv in case of VERSION argument. */
if
(
v
==
verstr
)
{
c
->
argv
-=
2
;
c
->
argc
+=
2
;
}
}
/* ========================== LOLWUT Canvase ===============================
* Many LOWUT versions will likely print some computer art to the screen.
* This is the case with LOLWUT 5 and LOLWUT 6, so here there is a generic
* canvas implementation that can be reused. */
/* Allocate and return a new canvas of the specified size. */
lwCanvas
*
lwCreateCanvas
(
int
width
,
int
height
,
int
bgcolor
)
{
lwCanvas
*
canvas
=
zmalloc
(
sizeof
(
*
canvas
));
canvas
->
width
=
width
;
canvas
->
height
=
height
;
canvas
->
pixels
=
zmalloc
(
width
*
height
);
memset
(
canvas
->
pixels
,
bgcolor
,
width
*
height
);
return
canvas
;
}
/* Free the canvas created by lwCreateCanvas(). */
void
lwFreeCanvas
(
lwCanvas
*
canvas
)
{
zfree
(
canvas
->
pixels
);
zfree
(
canvas
);
}
/* Set a pixel to the specified color. Color is 0 or 1, where zero means no
* dot will be displyed, and 1 means dot will be displayed.
* Coordinates are arranged so that left-top corner is 0,0. You can write
* out of the size of the canvas without issues. */
void
lwDrawPixel
(
lwCanvas
*
canvas
,
int
x
,
int
y
,
int
color
)
{
if
(
x
<
0
||
x
>=
canvas
->
width
||
y
<
0
||
y
>=
canvas
->
height
)
return
;
canvas
->
pixels
[
x
+
y
*
canvas
->
width
]
=
color
;
}
/* Return the value of the specified pixel on the canvas. */
int
lwGetPixel
(
lwCanvas
*
canvas
,
int
x
,
int
y
)
{
if
(
x
<
0
||
x
>=
canvas
->
width
||
y
<
0
||
y
>=
canvas
->
height
)
return
0
;
return
canvas
->
pixels
[
x
+
y
*
canvas
->
width
];
}
/* Draw a line from x1,y1 to x2,y2 using the Bresenham algorithm. */
void
lwDrawLine
(
lwCanvas
*
canvas
,
int
x1
,
int
y1
,
int
x2
,
int
y2
,
int
color
)
{
int
dx
=
abs
(
x2
-
x1
);
int
dy
=
abs
(
y2
-
y1
);
int
sx
=
(
x1
<
x2
)
?
1
:
-
1
;
int
sy
=
(
y1
<
y2
)
?
1
:
-
1
;
int
err
=
dx
-
dy
,
e2
;
while
(
1
)
{
lwDrawPixel
(
canvas
,
x1
,
y1
,
color
);
if
(
x1
==
x2
&&
y1
==
y2
)
break
;
e2
=
err
*
2
;
if
(
e2
>
-
dy
)
{
err
-=
dy
;
x1
+=
sx
;
}
if
(
e2
<
dx
)
{
err
+=
dx
;
y1
+=
sy
;
}
}
}
/* Draw a square centered at the specified x,y coordinates, with the specified
* rotation angle and size. In order to write a rotated square, we use the
* trivial fact that the parametric equation:
*
* x = sin(k)
* y = cos(k)
*
* Describes a circle for values going from 0 to 2*PI. So basically if we start
* at 45 degrees, that is k = PI/4, with the first point, and then we find
* the other three points incrementing K by PI/2 (90 degrees), we'll have the
* points of the square. In order to rotate the square, we just start with
* k = PI/4 + rotation_angle, and we are done.
*
* Of course the vanilla equations above will describe the square inside a
* circle of radius 1, so in order to draw larger squares we'll have to
* multiply the obtained coordinates, and then translate them. However this
* is much simpler than implementing the abstract concept of 2D shape and then
* performing the rotation/translation transformation, so for LOLWUT it's
* a good approach. */
void
lwDrawSquare
(
lwCanvas
*
canvas
,
int
x
,
int
y
,
float
size
,
float
angle
,
int
color
)
{
int
px
[
4
],
py
[
4
];
/* Adjust the desired size according to the fact that the square inscribed
* into a circle of radius 1 has the side of length SQRT(2). This way
* size becomes a simple multiplication factor we can use with our
* coordinates to magnify them. */
size
/=
1
.
4142135623
;
size
=
round
(
size
);
/* Compute the four points. */
float
k
=
M_PI
/
4
+
angle
;
for
(
int
j
=
0
;
j
<
4
;
j
++
)
{
px
[
j
]
=
round
(
sin
(
k
)
*
size
+
x
);
py
[
j
]
=
round
(
cos
(
k
)
*
size
+
y
);
k
+=
M_PI
/
2
;
}
/* Draw the square. */
for
(
int
j
=
0
;
j
<
4
;
j
++
)
lwDrawLine
(
canvas
,
px
[
j
],
py
[
j
],
px
[(
j
+
1
)
%
4
],
py
[(
j
+
1
)
%
4
],
color
);
}
src/lolwut.h
0 → 100644
View file @
2968d8e3
/*
* Copyright (c) 2018-2019, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Redis nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/* This structure represents our canvas. Drawing functions will take a pointer
* to a canvas to write to it. Later the canvas can be rendered to a string
* suitable to be printed on the screen, using unicode Braille characters. */
/* This represents a very simple generic canvas in order to draw stuff.
* It's up to each LOLWUT versions to translate what they draw to the
* screen, depending on the result to accomplish. */
typedef
struct
lwCanvas
{
int
width
;
int
height
;
char
*
pixels
;
}
lwCanvas
;
/* Drawing functions implemented inside lolwut.c. */
lwCanvas
*
lwCreateCanvas
(
int
width
,
int
height
,
int
bgcolor
);
void
lwFreeCanvas
(
lwCanvas
*
canvas
);
void
lwDrawPixel
(
lwCanvas
*
canvas
,
int
x
,
int
y
,
int
color
);
int
lwGetPixel
(
lwCanvas
*
canvas
,
int
x
,
int
y
);
void
lwDrawLine
(
lwCanvas
*
canvas
,
int
x1
,
int
y1
,
int
x2
,
int
y2
,
int
color
);
void
lwDrawSquare
(
lwCanvas
*
canvas
,
int
x
,
int
y
,
float
size
,
float
angle
,
int
color
);
src/lolwut5.c
View file @
2968d8e3
...
...
@@ -34,17 +34,9 @@
*/
#include "server.h"
#include "lolwut.h"
#include <math.h>
/* This structure represents our canvas. Drawing functions will take a pointer
* to a canvas to write to it. Later the canvas can be rendered to a string
* suitable to be printed on the screen, using unicode Braille characters. */
typedef
struct
lwCanvas
{
int
width
;
int
height
;
char
*
pixels
;
}
lwCanvas
;
/* Translate a group of 8 pixels (2x4 vertical rectangle) to the corresponding
* braille character. The byte should correspond to the pixels arranged as
* follows, where 0 is the least significant bit, and 7 the most significant
...
...
@@ -69,104 +61,6 @@ void lwTranslatePixelsGroup(int byte, char *output) {
output
[
2
]
=
0x80
|
(
code
&
0x3F
);
/* 10-xxxxxx */
}
/* Allocate and return a new canvas of the specified size. */
lwCanvas
*
lwCreateCanvas
(
int
width
,
int
height
)
{
lwCanvas
*
canvas
=
zmalloc
(
sizeof
(
*
canvas
));
canvas
->
width
=
width
;
canvas
->
height
=
height
;
canvas
->
pixels
=
zmalloc
(
width
*
height
);
memset
(
canvas
->
pixels
,
0
,
width
*
height
);
return
canvas
;
}
/* Free the canvas created by lwCreateCanvas(). */
void
lwFreeCanvas
(
lwCanvas
*
canvas
)
{
zfree
(
canvas
->
pixels
);
zfree
(
canvas
);
}
/* Set a pixel to the specified color. Color is 0 or 1, where zero means no
* dot will be displyed, and 1 means dot will be displayed.
* Coordinates are arranged so that left-top corner is 0,0. You can write
* out of the size of the canvas without issues. */
void
lwDrawPixel
(
lwCanvas
*
canvas
,
int
x
,
int
y
,
int
color
)
{
if
(
x
<
0
||
x
>=
canvas
->
width
||
y
<
0
||
y
>=
canvas
->
height
)
return
;
canvas
->
pixels
[
x
+
y
*
canvas
->
width
]
=
color
;
}
/* Return the value of the specified pixel on the canvas. */
int
lwGetPixel
(
lwCanvas
*
canvas
,
int
x
,
int
y
)
{
if
(
x
<
0
||
x
>=
canvas
->
width
||
y
<
0
||
y
>=
canvas
->
height
)
return
0
;
return
canvas
->
pixels
[
x
+
y
*
canvas
->
width
];
}
/* Draw a line from x1,y1 to x2,y2 using the Bresenham algorithm. */
void
lwDrawLine
(
lwCanvas
*
canvas
,
int
x1
,
int
y1
,
int
x2
,
int
y2
,
int
color
)
{
int
dx
=
abs
(
x2
-
x1
);
int
dy
=
abs
(
y2
-
y1
);
int
sx
=
(
x1
<
x2
)
?
1
:
-
1
;
int
sy
=
(
y1
<
y2
)
?
1
:
-
1
;
int
err
=
dx
-
dy
,
e2
;
while
(
1
)
{
lwDrawPixel
(
canvas
,
x1
,
y1
,
color
);
if
(
x1
==
x2
&&
y1
==
y2
)
break
;
e2
=
err
*
2
;
if
(
e2
>
-
dy
)
{
err
-=
dy
;
x1
+=
sx
;
}
if
(
e2
<
dx
)
{
err
+=
dx
;
y1
+=
sy
;
}
}
}
/* Draw a square centered at the specified x,y coordinates, with the specified
* rotation angle and size. In order to write a rotated square, we use the
* trivial fact that the parametric equation:
*
* x = sin(k)
* y = cos(k)
*
* Describes a circle for values going from 0 to 2*PI. So basically if we start
* at 45 degrees, that is k = PI/4, with the first point, and then we find
* the other three points incrementing K by PI/2 (90 degrees), we'll have the
* points of the square. In order to rotate the square, we just start with
* k = PI/4 + rotation_angle, and we are done.
*
* Of course the vanilla equations above will describe the square inside a
* circle of radius 1, so in order to draw larger squares we'll have to
* multiply the obtained coordinates, and then translate them. However this
* is much simpler than implementing the abstract concept of 2D shape and then
* performing the rotation/translation transformation, so for LOLWUT it's
* a good approach. */
void
lwDrawSquare
(
lwCanvas
*
canvas
,
int
x
,
int
y
,
float
size
,
float
angle
)
{
int
px
[
4
],
py
[
4
];
/* Adjust the desired size according to the fact that the square inscribed
* into a circle of radius 1 has the side of length SQRT(2). This way
* size becomes a simple multiplication factor we can use with our
* coordinates to magnify them. */
size
/=
1
.
4142135623
;
size
=
round
(
size
);
/* Compute the four points. */
float
k
=
M_PI
/
4
+
angle
;
for
(
int
j
=
0
;
j
<
4
;
j
++
)
{
px
[
j
]
=
round
(
sin
(
k
)
*
size
+
x
);
py
[
j
]
=
round
(
cos
(
k
)
*
size
+
y
);
k
+=
M_PI
/
2
;
}
/* Draw the square. */
for
(
int
j
=
0
;
j
<
4
;
j
++
)
lwDrawLine
(
canvas
,
px
[
j
],
py
[
j
],
px
[(
j
+
1
)
%
4
],
py
[(
j
+
1
)
%
4
],
1
);
}
/* Schotter, the output of LOLWUT of Redis 5, is a computer graphic art piece
* generated by Georg Nees in the 60s. It explores the relationship between
* caos and order.
...
...
@@ -180,7 +74,7 @@ lwCanvas *lwDrawSchotter(int console_cols, int squares_per_row, int squares_per_
int
padding
=
canvas_width
>
4
?
2
:
0
;
float
square_side
=
(
float
)(
canvas_width
-
padding
*
2
)
/
squares_per_row
;
int
canvas_height
=
square_side
*
squares_per_col
+
padding
*
2
;
lwCanvas
*
canvas
=
lwCreateCanvas
(
canvas_width
,
canvas_height
);
lwCanvas
*
canvas
=
lwCreateCanvas
(
canvas_width
,
canvas_height
,
0
);
for
(
int
y
=
0
;
y
<
squares_per_col
;
y
++
)
{
for
(
int
x
=
0
;
x
<
squares_per_row
;
x
++
)
{
...
...
@@ -200,7 +94,7 @@ lwCanvas *lwDrawSchotter(int console_cols, int squares_per_row, int squares_per_
sx
+=
r2
*
square_side
/
3
;
sy
+=
r3
*
square_side
/
3
;
}
lwDrawSquare
(
canvas
,
sx
,
sy
,
square_side
,
angle
);
lwDrawSquare
(
canvas
,
sx
,
sy
,
square_side
,
angle
,
1
);
}
}
...
...
@@ -212,7 +106,7 @@ lwCanvas *lwDrawSchotter(int console_cols, int squares_per_row, int squares_per_
* logical canvas. The actual returned string will require a terminal that is
* width/2 large and height/4 tall in order to hold the whole image without
* overflowing or scrolling, since each Barille character is 2x4. */
sds
lwR
enderCanvas
(
lwCanvas
*
canvas
)
{
static
sds
r
enderCanvas
(
lwCanvas
*
canvas
)
{
sds
text
=
sdsempty
();
for
(
int
y
=
0
;
y
<
canvas
->
height
;
y
+=
4
)
{
for
(
int
x
=
0
;
x
<
canvas
->
width
;
x
+=
2
)
{
...
...
@@ -272,11 +166,12 @@ void lolwut5Command(client *c) {
/* Generate some computer art and reply. */
lwCanvas
*
canvas
=
lwDrawSchotter
(
cols
,
squares_per_row
,
squares_per_col
);
sds
rendered
=
lwR
enderCanvas
(
canvas
);
sds
rendered
=
r
enderCanvas
(
canvas
);
rendered
=
sdscat
(
rendered
,
"
\n
Georg Nees - schotter, plotter on paper, 1968. Redis ver. "
);
rendered
=
sdscat
(
rendered
,
REDIS_VERSION
);
rendered
=
sdscatlen
(
rendered
,
"
\n
"
,
1
);
addReplyBulkSds
(
c
,
rendered
);
addReplyVerbatim
(
c
,
rendered
,
sdslen
(
rendered
),
"txt"
);
sdsfree
(
rendered
);
lwFreeCanvas
(
canvas
);
}
src/lolwut6.c
0 → 100644
View file @
2968d8e3
/*
* Copyright (c) 2019, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Redis nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* ----------------------------------------------------------------------------
*
* This file implements the LOLWUT command. The command should do something
* fun and interesting, and should be replaced by a new implementation at
* each new version of Redis.
*
* Thanks to Michele Hiki Falcone for the original image that ispired
* the image, part of his game, Plaguemon.
*
* Thanks to the Shhh computer art collective for the help in tuning the
* output to have a better artistic effect.
*/
#include "server.h"
#include "lolwut.h"
/* Render the canvas using the four gray levels of the standard color
* terminal: they match very well to the grayscale display of the gameboy. */
static
sds
renderCanvas
(
lwCanvas
*
canvas
)
{
sds
text
=
sdsempty
();
for
(
int
y
=
0
;
y
<
canvas
->
height
;
y
++
)
{
for
(
int
x
=
0
;
x
<
canvas
->
width
;
x
++
)
{
int
color
=
lwGetPixel
(
canvas
,
x
,
y
);
char
*
ce
;
/* Color escape sequence. */
/* Note that we set both the foreground and background color.
* This way we are able to get a more consistent result among
* different terminals implementations. */
switch
(
color
)
{
case
0
:
ce
=
"0;30;40m"
;
break
;
/* Black */
case
1
:
ce
=
"0;90;100m"
;
break
;
/* Gray 1 */
case
2
:
ce
=
"0;37;47m"
;
break
;
/* Gray 2 */
case
3
:
ce
=
"0;97;107m"
;
break
;
/* White */
default:
ce
=
"0;30;40m"
;
break
;
/* Just for safety. */
}
text
=
sdscatprintf
(
text
,
"
\033
[%s
\033
[0m"
,
ce
);
}
if
(
y
!=
canvas
->
height
-
1
)
text
=
sdscatlen
(
text
,
"
\n
"
,
1
);
}
return
text
;
}
/* Draw a skyscraper on the canvas, according to the parameters in the
* 'skyscraper' structure. Window colors are random and are always one
* of the two grays. */
struct
skyscraper
{
int
xoff
;
/* X offset. */
int
width
;
/* Pixels width. */
int
height
;
/* Pixels height. */
int
windows
;
/* Draw windows if true. */
int
color
;
/* Color of the skyscraper. */
};
void
generateSkyscraper
(
lwCanvas
*
canvas
,
struct
skyscraper
*
si
)
{
int
starty
=
canvas
->
height
-
1
;
int
endy
=
starty
-
si
->
height
+
1
;
for
(
int
y
=
starty
;
y
>=
endy
;
y
--
)
{
for
(
int
x
=
si
->
xoff
;
x
<
si
->
xoff
+
si
->
width
;
x
++
)
{
/* The roof is four pixels less wide. */
if
(
y
==
endy
&&
(
x
<=
si
->
xoff
+
1
||
x
>=
si
->
xoff
+
si
->
width
-
2
))
continue
;
int
color
=
si
->
color
;
/* Alter the color if this is a place where we want to
* draw a window. We check that we are in the inner part of the
* skyscraper, so that windows are far from the borders. */
if
(
si
->
windows
&&
x
>
si
->
xoff
+
1
&&
x
<
si
->
xoff
+
si
->
width
-
2
&&
y
>
endy
+
1
&&
y
<
starty
-
1
)
{
/* Calculate the x,y position relative to the start of
* the window area. */
int
relx
=
x
-
(
si
->
xoff
+
1
);
int
rely
=
y
-
(
endy
+
1
);
/* Note that we want the windows to be two pixels wide
* but just one pixel tall, because terminal "pixels"
* (characters) are not square. */
if
(
relx
/
2
%
2
&&
rely
%
2
)
{
do
{
color
=
1
+
rand
()
%
2
;
}
while
(
color
==
si
->
color
);
/* Except we want adjacent pixels creating the same
* window to be the same color. */
if
(
relx
%
2
)
color
=
lwGetPixel
(
canvas
,
x
-
1
,
y
);
}
}
lwDrawPixel
(
canvas
,
x
,
y
,
color
);
}
}
}
/* Generate a skyline inspired by the parallax backgrounds of 8 bit games. */
void
generateSkyline
(
lwCanvas
*
canvas
)
{
struct
skyscraper
si
;
/* First draw the background skyscraper without windows, using the
* two different grays. We use two passes to make sure that the lighter
* ones are always in the background. */
for
(
int
color
=
2
;
color
>=
1
;
color
--
)
{
si
.
color
=
color
;
for
(
int
offset
=
-
10
;
offset
<
canvas
->
width
;)
{
offset
+=
rand
()
%
8
;
si
.
xoff
=
offset
;
si
.
width
=
10
+
rand
()
%
9
;
if
(
color
==
2
)
si
.
height
=
canvas
->
height
/
2
+
rand
()
%
canvas
->
height
/
2
;
else
si
.
height
=
canvas
->
height
/
2
+
rand
()
%
canvas
->
height
/
3
;
si
.
windows
=
0
;
generateSkyscraper
(
canvas
,
&
si
);
if
(
color
==
2
)
offset
+=
si
.
width
/
2
;
else
offset
+=
si
.
width
+
1
;
}
}
/* Now draw the foreground skyscraper with the windows. */
si
.
color
=
0
;
for
(
int
offset
=
-
10
;
offset
<
canvas
->
width
;)
{
offset
+=
rand
()
%
8
;
si
.
xoff
=
offset
;
si
.
width
=
5
+
rand
()
%
14
;
if
(
si
.
width
%
4
)
si
.
width
+=
(
si
.
width
%
3
);
si
.
height
=
canvas
->
height
/
3
+
rand
()
%
canvas
->
height
/
2
;
si
.
windows
=
1
;
generateSkyscraper
(
canvas
,
&
si
);
offset
+=
si
.
width
+
5
;
}
}
/* The LOLWUT 6 command:
*
* LOLWUT [columns] [rows]
*
* By default the command uses 80 columns, 40 squares per row
* per column.
*/
void
lolwut6Command
(
client
*
c
)
{
long
cols
=
80
;
long
rows
=
20
;
/* Parse the optional arguments if any. */
if
(
c
->
argc
>
1
&&
getLongFromObjectOrReply
(
c
,
c
->
argv
[
1
],
&
cols
,
NULL
)
!=
C_OK
)
return
;
if
(
c
->
argc
>
2
&&
getLongFromObjectOrReply
(
c
,
c
->
argv
[
2
],
&
rows
,
NULL
)
!=
C_OK
)
return
;
/* Limits. We want LOLWUT to be always reasonably fast and cheap to execute
* so we have maximum number of columns, rows, and output resulution. */
if
(
cols
<
1
)
cols
=
1
;
if
(
cols
>
1000
)
cols
=
1000
;
if
(
rows
<
1
)
rows
=
1
;
if
(
rows
>
1000
)
rows
=
1000
;
/* Generate the city skyline and reply. */
lwCanvas
*
canvas
=
lwCreateCanvas
(
cols
,
rows
,
3
);
generateSkyline
(
canvas
);
sds
rendered
=
renderCanvas
(
canvas
);
rendered
=
sdscat
(
rendered
,
"
\n
Dedicated to the 8 bit game developers of past and present.
\n
"
"Original 8 bit image from Plaguemon by hikikomori. Redis ver. "
);
rendered
=
sdscat
(
rendered
,
REDIS_VERSION
);
rendered
=
sdscatlen
(
rendered
,
"
\n
"
,
1
);
addReplyVerbatim
(
c
,
rendered
,
sdslen
(
rendered
),
"txt"
);
sdsfree
(
rendered
);
lwFreeCanvas
(
canvas
);
}
src/mkreleasehdr.sh
View file @
2968d8e3
...
...
@@ -3,7 +3,7 @@ GIT_SHA1=`(git show-ref --head --hash=8 2> /dev/null || echo 00000000) | head -n
GIT_DIRTY
=
`
git diff
--no-ext-diff
2> /dev/null |
wc
-l
`
BUILD_ID
=
`
uname
-n
`
"-"
`
date
+%s
`
if
[
-n
"
$SOURCE_DATE_EPOCH
"
]
;
then
BUILD_ID
=
$(
date
-u
-d
"@
$SOURCE_DATE_EPOCH
"
+%s 2>/dev/null
||
date
-u
-r
"
$SOURCE_DATE_EPOCH
"
+%s 2>/dev/null
||
date
-u
%s
)
BUILD_ID
=
$(
date
-u
-d
"@
$SOURCE_DATE_EPOCH
"
+%s 2>/dev/null
||
date
-u
-r
"
$SOURCE_DATE_EPOCH
"
+%s 2>/dev/null
||
date
-u
+
%s
)
fi
test
-f
release.h
||
touch
release.h
(
cat
release.h |
grep
SHA1 |
grep
$GIT_SHA1
)
&&
\
...
...
src/module.c
View file @
2968d8e3
...
...
@@ -29,11 +29,10 @@
#include "server.h"
#include "cluster.h"
#include "rdb.h"
#include <dlfcn.h>
#include <sys/stat.h>
#define REDISMODULE_CORE 1
#include "redismodule.h"
#include <sys/wait.h>
/* --------------------------------------------------------------------------
* Private data structures used by the modules system. Those are data
...
...
@@ -41,6 +40,17 @@
* pointers that have an API the module can call with them)
* -------------------------------------------------------------------------- */
typedef
struct
RedisModuleInfoCtx
{
struct
RedisModule
*
module
;
const
char
*
requested_section
;
sds
info
;
/* info string we collected so far */
int
sections
;
/* number of sections we collected so far */
int
in_section
;
/* indication if we're in an active section or not */
int
in_dict_field
;
/* indication that we're curreintly appending to a dict */
}
RedisModuleInfoCtx
;
typedef
void
(
*
RedisModuleInfoFunc
)(
RedisModuleInfoCtx
*
ctx
,
int
for_crash_report
);
/* This structure represents a module inside the system. */
struct
RedisModule
{
void
*
handle
;
/* Module dlopen() handle. */
...
...
@@ -52,6 +62,10 @@ struct RedisModule {
list
*
using
;
/* List of modules we use some APIs of. */
list
*
filters
;
/* List of filters the module has registered. */
int
in_call
;
/* RM_Call() nesting level */
int
in_hook
;
/* Hooks callback nesting level for this module (0 or 1). */
int
options
;
/* Module options and capabilities. */
int
blocked_clients
;
/* Count of RedisModuleBlockedClient in this module. */
RedisModuleInfoFunc
info_cb
;
/* Callback for module to add INFO fields. */
};
typedef
struct
RedisModule
RedisModule
;
...
...
@@ -80,6 +94,7 @@ struct AutoMemEntry {
#define REDISMODULE_AM_REPLY 2
#define REDISMODULE_AM_FREED 3
/* Explicitly freed by user already. */
#define REDISMODULE_AM_DICT 4
#define REDISMODULE_AM_INFO 5
/* The pool allocator block. Redis Modules can allocate memory via this special
* allocator that will automatically release it all once the callback returns.
...
...
@@ -127,16 +142,23 @@ struct RedisModuleCtx {
void
**
postponed_arrays
;
/* To set with RM_ReplySetArrayLength(). */
int
postponed_arrays_count
;
/* Number of entries in postponed_arrays. */
void
*
blocked_privdata
;
/* Privdata set when unblocking a client. */
RedisModuleString
*
blocked_ready_key
;
/* Key ready when the reply callback
gets called for clients blocked
on keys. */
/* Used if there is the REDISMODULE_CTX_KEYS_POS_REQUEST flag set. */
int
*
keys_pos
;
int
keys_count
;
struct
RedisModulePoolAllocBlock
*
pa_head
;
redisOpArray
saved_oparray
;
/* When propagating commands in a callback
we reallocate the "also propagate" op
array. Here we save the old one to
restore it later. */
};
typedef
struct
RedisModuleCtx
RedisModuleCtx
;
#define REDISMODULE_CTX_INIT {(void*)(unsigned long)&RM_GetApi, NULL, NULL, NULL, NULL, 0, 0, 0, NULL, 0, NULL, NULL, 0, NULL}
#define REDISMODULE_CTX_INIT {(void*)(unsigned long)&RM_GetApi, NULL, NULL, NULL, NULL, 0, 0, 0, NULL, 0, NULL, NULL,
NULL,
0, NULL
, {0}
}
#define REDISMODULE_CTX_MULTI_EMITTED (1<<0)
#define REDISMODULE_CTX_AUTO_MEMORY (1<<1)
#define REDISMODULE_CTX_KEYS_POS_REQUEST (1<<2)
...
...
@@ -144,6 +166,7 @@ typedef struct RedisModuleCtx RedisModuleCtx;
#define REDISMODULE_CTX_BLOCKED_TIMEOUT (1<<4)
#define REDISMODULE_CTX_THREAD_SAFE (1<<5)
#define REDISMODULE_CTX_BLOCKED_DISCONNECTED (1<<6)
#define REDISMODULE_CTX_MODULE_COMMAND_CALL (1<<7)
/* This represents a Redis key opened with RM_OpenKey(). */
struct
RedisModuleKey
{
...
...
@@ -227,6 +250,8 @@ typedef struct RedisModuleBlockedClient {
client
*
reply_client
;
/* Fake client used to accumulate replies
in thread safe contexts. */
int
dbid
;
/* Database number selected by the original client. */
int
blocked_on_keys
;
/* If blocked via RM_BlockClientOnKeys(). */
int
unblocked
;
/* Already on the moduleUnblocked list. */
}
RedisModuleBlockedClient
;
static
pthread_mutex_t
moduleUnblockedClientsMutex
=
PTHREAD_MUTEX_INITIALIZER
;
...
...
@@ -292,6 +317,44 @@ typedef struct RedisModuleCommandFilter {
/* Registered filters */
static
list
*
moduleCommandFilters
;
typedef
void
(
*
RedisModuleForkDoneHandler
)
(
int
exitcode
,
int
bysignal
,
void
*
user_data
);
static
struct
RedisModuleForkInfo
{
RedisModuleForkDoneHandler
done_handler
;
void
*
done_handler_user_data
;
}
moduleForkInfo
=
{
0
};
typedef
struct
RedisModuleServerInfoData
{
rax
*
rax
;
/* parsed info data. */
}
RedisModuleServerInfoData
;
/* Flags for moduleCreateArgvFromUserFormat(). */
#define REDISMODULE_ARGV_REPLICATE (1<<0)
#define REDISMODULE_ARGV_NO_AOF (1<<1)
#define REDISMODULE_ARGV_NO_REPLICAS (1<<2)
/* Determine whether Redis should signalModifiedKey implicitly.
* In case 'ctx' has no 'module' member (and therefore no module->options),
* we assume default behavior, that is, Redis signals.
* (see RM_GetThreadSafeContext) */
#define SHOULD_SIGNAL_MODIFIED_KEYS(ctx) \
ctx->module? !(ctx->module->options & REDISMODULE_OPTION_NO_IMPLICIT_SIGNAL_MODIFIED) : 1
/* Server events hooks data structures and defines: this modules API
* allow modules to subscribe to certain events in Redis, such as
* the start and end of an RDB or AOF save, the change of role in replication,
* and similar other events. */
typedef
struct
RedisModuleEventListener
{
RedisModule
*
module
;
RedisModuleEvent
event
;
RedisModuleEventCallback
callback
;
}
RedisModuleEventListener
;
list
*
RedisModule_EventListeners
;
/* Global list of all the active events. */
unsigned
long
long
ModulesInHooks
=
0
;
/* Total number of modules in hooks
callbacks right now. */
/* --------------------------------------------------------------------------
* Prototypes
* -------------------------------------------------------------------------- */
...
...
@@ -304,6 +367,7 @@ void moduleReplicateMultiIfNeeded(RedisModuleCtx *ctx);
void
RM_ZsetRangeStop
(
RedisModuleKey
*
kp
);
static
void
zsetKeyReset
(
RedisModuleKey
*
key
);
void
RM_FreeDict
(
RedisModuleCtx
*
ctx
,
RedisModuleDict
*
d
);
void
RM_FreeServerInfo
(
RedisModuleCtx
*
ctx
,
RedisModuleServerInfoData
*
data
);
/* --------------------------------------------------------------------------
* Heap allocation raw functions
...
...
@@ -461,7 +525,8 @@ int moduleDelKeyIfEmpty(RedisModuleKey *key) {
case
OBJ_LIST
:
isempty
=
listTypeLength
(
o
)
==
0
;
break
;
case
OBJ_SET
:
isempty
=
setTypeSize
(
o
)
==
0
;
break
;
case
OBJ_ZSET
:
isempty
=
zsetLength
(
o
)
==
0
;
break
;
case
OBJ_HASH
:
isempty
=
hashTypeLength
(
o
)
==
0
;
break
;
case
OBJ_HASH
:
isempty
=
hashTypeLength
(
o
)
==
0
;
break
;
case
OBJ_STREAM
:
isempty
=
streamLength
(
o
)
==
0
;
break
;
default:
isempty
=
0
;
}
...
...
@@ -497,51 +562,72 @@ int RM_GetApi(const char *funcname, void **targetPtrPtr) {
return
REDISMODULE_OK
;
}
/* Free the context after the user function was called. */
void
moduleFreeContext
(
RedisModuleCtx
*
ctx
)
{
autoMemoryCollect
(
ctx
);
poolAllocRelease
(
ctx
);
if
(
ctx
->
postponed_arrays
)
{
zfree
(
ctx
->
postponed_arrays
);
ctx
->
postponed_arrays_count
=
0
;
serverLog
(
LL_WARNING
,
"API misuse detected in module %s: "
"RedisModule_ReplyWithArray(REDISMODULE_POSTPONED_ARRAY_LEN) "
"not matched by the same number of RedisModule_SetReplyArrayLen() "
"calls."
,
ctx
->
module
->
name
);
}
if
(
ctx
->
flags
&
REDISMODULE_CTX_THREAD_SAFE
)
freeClient
(
ctx
->
client
);
}
/* Helper function for when a command callback is called, in order to handle
* details needed to correctly replicate commands. */
void
moduleHandlePropagationAfterCommandCallback
(
RedisModuleCtx
*
ctx
)
{
client
*
c
=
ctx
->
client
;
/* We don't need to do anything here if the context was never used
* in order to propagate commands. */
if
(
!
(
ctx
->
flags
&
REDISMODULE_CTX_MULTI_EMITTED
))
return
;
if
(
c
->
flags
&
CLIENT_LUA
)
return
;
/* Handle the replication of the final EXEC, since whatever a command
* emits is always wrapped around MULTI/EXEC. */
if
(
ctx
->
flags
&
REDISMODULE_CTX_MULTI_EMITTED
)
{
robj
*
propargv
[
1
];
propargv
[
0
]
=
createStringObject
(
"EXEC"
,
4
);
alsoPropagate
(
server
.
execCommand
,
c
->
db
->
id
,
propargv
,
1
,
PROPAGATE_AOF
|
PROPAGATE_REPL
);
decrRefCount
(
propargv
[
0
]);
/* If this is not a module command context (but is instead a simple
* callback context), we have to handle directly the "also propagate"
* array and emit it. In a module command call this will be handled
* directly by call(). */
if
(
!
(
ctx
->
flags
&
REDISMODULE_CTX_MODULE_COMMAND_CALL
)
&&
server
.
also_propagate
.
numops
)
{
for
(
int
j
=
0
;
j
<
server
.
also_propagate
.
numops
;
j
++
)
{
redisOp
*
rop
=
&
server
.
also_propagate
.
ops
[
j
];
int
target
=
rop
->
target
;
if
(
target
)
propagate
(
rop
->
cmd
,
rop
->
dbid
,
rop
->
argv
,
rop
->
argc
,
target
);
}
redisOpArrayFree
(
&
server
.
also_propagate
);
/* Restore the previous oparray in case of nexted use of the API. */
server
.
also_propagate
=
ctx
->
saved_oparray
;
}
}
/* Free the context after the user function was called. */
void
moduleFreeContext
(
RedisModuleCtx
*
ctx
)
{
moduleHandlePropagationAfterCommandCallback
(
ctx
);
autoMemoryCollect
(
ctx
);
poolAllocRelease
(
ctx
);
if
(
ctx
->
postponed_arrays
)
{
zfree
(
ctx
->
postponed_arrays
);
ctx
->
postponed_arrays_count
=
0
;
serverLog
(
LL_WARNING
,
"API misuse detected in module %s: "
"RedisModule_ReplyWithArray(REDISMODULE_POSTPONED_ARRAY_LEN) "
"not matched by the same number of RedisModule_SetReplyArrayLen() "
"calls."
,
ctx
->
module
->
name
);
}
if
(
ctx
->
flags
&
REDISMODULE_CTX_THREAD_SAFE
)
freeClient
(
ctx
->
client
);
}
/* This Redis command binds the normal Redis command invocation with commands
* exported by modules. */
void
RedisModuleCommandDispatcher
(
client
*
c
)
{
RedisModuleCommandProxy
*
cp
=
(
void
*
)(
unsigned
long
)
c
->
cmd
->
getkeys_proc
;
RedisModuleCtx
ctx
=
REDISMODULE_CTX_INIT
;
ctx
.
flags
|=
REDISMODULE_CTX_MODULE_COMMAND_CALL
;
ctx
.
module
=
cp
->
module
;
ctx
.
client
=
c
;
cp
->
func
(
&
ctx
,(
void
**
)
c
->
argv
,
c
->
argc
);
moduleHandlePropagationAfterCommandCallback
(
&
ctx
);
moduleFreeContext
(
&
ctx
);
/* In some cases processMultibulkBuffer uses sdsMakeRoomFor to
...
...
@@ -755,6 +841,8 @@ void RM_SetModuleAttribs(RedisModuleCtx *ctx, const char *name, int ver, int api
module
->
using
=
listCreate
();
module
->
filters
=
listCreate
();
module
->
in_call
=
0
;
module
->
in_hook
=
0
;
module
->
options
=
0
;
ctx
->
module
=
module
;
}
...
...
@@ -772,6 +860,25 @@ long long RM_Milliseconds(void) {
return
mstime
();
}
/* Set flags defining capabilities or behavior bit flags.
*
* REDISMODULE_OPTIONS_HANDLE_IO_ERRORS:
* Generally, modules don't need to bother with this, as the process will just
* terminate if a read error happens, however, setting this flag would allow
* repl-diskless-load to work if enabled.
* The module should use RedisModule_IsIOError after reads, before using the
* data that was read, and in case of error, propagate it upwards, and also be
* able to release the partially populated value and all it's allocations. */
void
RM_SetModuleOptions
(
RedisModuleCtx
*
ctx
,
int
options
)
{
ctx
->
module
->
options
=
options
;
}
/* Signals that the key is modified from user's perspective (i.e. invalidate WATCH). */
int
RM_SignalModifiedKey
(
RedisModuleCtx
*
ctx
,
RedisModuleString
*
keyname
)
{
signalModifiedKey
(
ctx
->
client
->
db
,
keyname
);
return
REDISMODULE_OK
;
}
/* --------------------------------------------------------------------------
* Automatic memory management for modules
* -------------------------------------------------------------------------- */
...
...
@@ -847,6 +954,7 @@ void autoMemoryCollect(RedisModuleCtx *ctx) {
case
REDISMODULE_AM_REPLY
:
RM_FreeCallReply
(
ptr
);
break
;
case
REDISMODULE_AM_KEY
:
RM_CloseKey
(
ptr
);
break
;
case
REDISMODULE_AM_DICT
:
RM_FreeDict
(
NULL
,
ptr
);
break
;
case
REDISMODULE_AM_INFO
:
RM_FreeServerInfo
(
NULL
,
ptr
);
break
;
}
}
ctx
->
flags
|=
REDISMODULE_CTX_AUTO_MEMORY
;
...
...
@@ -913,6 +1021,21 @@ RedisModuleString *RM_CreateStringFromLongLong(RedisModuleCtx *ctx, long long ll
return
RM_CreateString
(
ctx
,
buf
,
len
);
}
/* Like RedisModule_CreatString(), but creates a string starting from a long
* double.
*
* The returned string must be released with RedisModule_FreeString() or by
* enabling automatic memory management.
*
* The passed context 'ctx' may be NULL if necessary, see the
* RedisModule_CreateString() documentation for more info. */
RedisModuleString
*
RM_CreateStringFromLongDouble
(
RedisModuleCtx
*
ctx
,
long
double
ld
,
int
humanfriendly
)
{
char
buf
[
MAX_LONG_DOUBLE_CHARS
];
size_t
len
=
ld2string
(
buf
,
sizeof
(
buf
),
ld
,
(
humanfriendly
?
LD_STR_HUMAN
:
LD_STR_AUTO
));
return
RM_CreateString
(
ctx
,
buf
,
len
);
}
/* Like RedisModule_CreatString(), but creates a string starting from another
* RedisModuleString.
*
...
...
@@ -1017,6 +1140,14 @@ int RM_StringToDouble(const RedisModuleString *str, double *d) {
return
(
retval
==
C_OK
)
?
REDISMODULE_OK
:
REDISMODULE_ERR
;
}
/* Convert the string into a long double, storing it at `*ld`.
* Returns REDISMODULE_OK on success or REDISMODULE_ERR if the string is
* not a valid string representation of a double value. */
int
RM_StringToLongDouble
(
const
RedisModuleString
*
str
,
long
double
*
ld
)
{
int
retval
=
string2ld
(
str
->
ptr
,
sdslen
(
str
->
ptr
),
ld
);
return
retval
?
REDISMODULE_OK
:
REDISMODULE_ERR
;
}
/* Compare two string objects, returning -1, 0 or 1 respectively if
* a < b, a == b, a > b. Strings are compared byte by byte as two
* binary blobs without any encoding care / collation attempt. */
...
...
@@ -1126,10 +1257,9 @@ int RM_ReplyWithLongLong(RedisModuleCtx *ctx, long long ll) {
int
replyWithStatus
(
RedisModuleCtx
*
ctx
,
const
char
*
msg
,
char
*
prefix
)
{
client
*
c
=
moduleGetReplyClient
(
ctx
);
if
(
c
==
NULL
)
return
REDISMODULE_OK
;
sds
strmsg
=
sdsnewlen
(
prefix
,
1
);
strmsg
=
sdscat
(
strmsg
,
msg
);
strmsg
=
sdscatlen
(
strmsg
,
"
\r\n
"
,
2
);
addReplySds
(
c
,
strmsg
);
addReplyProto
(
c
,
prefix
,
strlen
(
prefix
));
addReplyProto
(
c
,
msg
,
strlen
(
msg
));
addReplyProto
(
c
,
"
\r\n
"
,
2
);
return
REDISMODULE_OK
;
}
...
...
@@ -1186,6 +1316,27 @@ int RM_ReplyWithArray(RedisModuleCtx *ctx, long len) {
return
REDISMODULE_OK
;
}
/* Reply to the client with a null array, simply null in RESP3
* null array in RESP2.
*
* The function always returns REDISMODULE_OK. */
int
RM_ReplyWithNullArray
(
RedisModuleCtx
*
ctx
)
{
client
*
c
=
moduleGetReplyClient
(
ctx
);
if
(
c
==
NULL
)
return
REDISMODULE_OK
;
addReplyNullArray
(
c
);
return
REDISMODULE_OK
;
}
/* Reply to the client with an empty array.
*
* The function always returns REDISMODULE_OK. */
int
RM_ReplyWithEmptyArray
(
RedisModuleCtx
*
ctx
)
{
client
*
c
=
moduleGetReplyClient
(
ctx
);
if
(
c
==
NULL
)
return
REDISMODULE_OK
;
addReply
(
c
,
shared
.
emptyarray
);
return
REDISMODULE_OK
;
}
/* When RedisModule_ReplyWithArray() is used with the argument
* REDISMODULE_POSTPONED_ARRAY_LEN, because we don't know beforehand the number
* of items we are going to output as elements of the array, this function
...
...
@@ -1264,8 +1415,28 @@ int RM_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str) {
return
REDISMODULE_OK
;
}
/* Reply to the client with a NULL. In the RESP protocol a NULL is encoded
* as the string "$-1\r\n".
/* Reply with an empty string.
*
* The function always returns REDISMODULE_OK. */
int
RM_ReplyWithEmptyString
(
RedisModuleCtx
*
ctx
)
{
client
*
c
=
moduleGetReplyClient
(
ctx
);
if
(
c
==
NULL
)
return
REDISMODULE_OK
;
addReply
(
c
,
shared
.
emptybulk
);
return
REDISMODULE_OK
;
}
/* Reply with a binary safe string, which should not be escaped or filtered
* taking in input a C buffer pointer and length.
*
* The function always returns REDISMODULE_OK. */
int
RM_ReplyWithVerbatimString
(
RedisModuleCtx
*
ctx
,
const
char
*
buf
,
size_t
len
)
{
client
*
c
=
moduleGetReplyClient
(
ctx
);
if
(
c
==
NULL
)
return
REDISMODULE_OK
;
addReplyVerbatim
(
c
,
buf
,
len
,
"txt"
);
return
REDISMODULE_OK
;
}
/* Reply to the client with a NULL.
*
* The function always returns REDISMODULE_OK. */
int
RM_ReplyWithNull
(
RedisModuleCtx
*
ctx
)
{
...
...
@@ -1302,6 +1473,21 @@ int RM_ReplyWithDouble(RedisModuleCtx *ctx, double d) {
return
REDISMODULE_OK
;
}
/* Send a string reply obtained converting the long double 'ld' into a bulk
* string. This function is basically equivalent to converting a long double
* into a string into a C buffer, and then calling the function
* RedisModule_ReplyWithStringBuffer() with the buffer and length.
* The double string uses human readable formatting (see
* `addReplyHumanLongDouble` in networking.c).
*
* The function always returns REDISMODULE_OK. */
int
RM_ReplyWithLongDouble
(
RedisModuleCtx
*
ctx
,
long
double
ld
)
{
client
*
c
=
moduleGetReplyClient
(
ctx
);
if
(
c
==
NULL
)
return
REDISMODULE_OK
;
addReplyHumanLongDouble
(
c
,
ld
);
return
REDISMODULE_OK
;
}
/* --------------------------------------------------------------------------
* Commands replication API
* -------------------------------------------------------------------------- */
...
...
@@ -1316,9 +1502,16 @@ void moduleReplicateMultiIfNeeded(RedisModuleCtx *ctx) {
/* If we already emitted MULTI return ASAP. */
if
(
ctx
->
flags
&
REDISMODULE_CTX_MULTI_EMITTED
)
return
;
/* If this is a thread safe context, we do not want to wrap commands
* executed into MU
T
LI/EXEC, they are executed as single commands
* executed into MUL
T
I/EXEC, they are executed as single commands
* from an external client in essence. */
if
(
ctx
->
flags
&
REDISMODULE_CTX_THREAD_SAFE
)
return
;
/* If this is a callback context, and not a module command execution
* context, we have to setup the op array for the "also propagate" API
* so that RM_Replicate() will work. */
if
(
!
(
ctx
->
flags
&
REDISMODULE_CTX_MODULE_COMMAND_CALL
))
{
ctx
->
saved_oparray
=
server
.
also_propagate
;
redisOpArrayInit
(
&
server
.
also_propagate
);
}
execCommandPropagateMulti
(
ctx
->
client
);
ctx
->
flags
|=
REDISMODULE_CTX_MULTI_EMITTED
;
}
...
...
@@ -1340,6 +1533,24 @@ void moduleReplicateMultiIfNeeded(RedisModuleCtx *ctx) {
*
* Please refer to RedisModule_Call() for more information.
*
* Using the special "A" and "R" modifiers, the caller can exclude either
* the AOF or the replicas from the propagation of the specified command.
* Otherwise, by default, the command will be propagated in both channels.
*
* ## Note about calling this function from a thread safe context:
*
* Normally when you call this function from the callback implementing a
* module command, or any other callback provided by the Redis Module API,
* Redis will accumulate all the calls to this function in the context of
* the callback, and will propagate all the commands wrapped in a MULTI/EXEC
* transaction. However when calling this function from a threaded safe context
* that can live an undefined amount of time, and can be locked/unlocked in
* at will, the behavior is different: MULTI/EXEC wrapper is not emitted
* and the command specified is inserted in the AOF and replication stream
* immediately.
*
* ## Return value
*
* The command returns REDISMODULE_ERR if the format specifiers are invalid
* or the command name does not belong to a known command. */
int
RM_Replicate
(
RedisModuleCtx
*
ctx
,
const
char
*
cmdname
,
const
char
*
fmt
,
...)
{
...
...
@@ -1357,10 +1568,23 @@ int RM_Replicate(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...)
va_end
(
ap
);
if
(
argv
==
NULL
)
return
REDISMODULE_ERR
;
/* Replicate! */
/* Select the propagation target. Usually is AOF + replicas, however
* the caller can exclude one or the other using the "A" or "R"
* modifiers. */
int
target
=
0
;
if
(
!
(
flags
&
REDISMODULE_ARGV_NO_AOF
))
target
|=
PROPAGATE_AOF
;
if
(
!
(
flags
&
REDISMODULE_ARGV_NO_REPLICAS
))
target
|=
PROPAGATE_REPL
;
/* Replicate! When we are in a threaded context, we want to just insert
* the replicated command ASAP, since it is not clear when the context
* will stop being used, so accumulating stuff does not make much sense,
* nor we could easily use the alsoPropagate() API from threads. */
if
(
ctx
->
flags
&
REDISMODULE_CTX_THREAD_SAFE
)
{
propagate
(
cmd
,
ctx
->
client
->
db
->
id
,
argv
,
argc
,
target
);
}
else
{
moduleReplicateMultiIfNeeded
(
ctx
);
alsoPropagate
(
cmd
,
ctx
->
client
->
db
->
id
,
argv
,
argc
,
PROPAGATE_AOF
|
PROPAGATE_REPL
);
alsoPropagate
(
cmd
,
ctx
->
client
->
db
->
id
,
argv
,
argc
,
target
);
}
/* Release the argv. */
for
(
j
=
0
;
j
<
argc
;
j
++
)
decrRefCount
(
argv
[
j
]);
...
...
@@ -1402,12 +1626,133 @@ int RM_ReplicateVerbatim(RedisModuleCtx *ctx) {
* are guaranteed to get IDs greater than any past ID previously seen.
*
* Valid IDs are from 1 to 2^64-1. If 0 is returned it means there is no way
* to fetch the ID in the context the function was currently called. */
* to fetch the ID in the context the function was currently called.
*
* After obtaining the ID, it is possible to check if the command execution
* is actually happening in the context of AOF loading, using this macro:
*
* if (RedisModule_IsAOFClient(RedisModule_GetClientId(ctx)) {
* // Handle it differently.
* }
*/
unsigned
long
long
RM_GetClientId
(
RedisModuleCtx
*
ctx
)
{
if
(
ctx
->
client
==
NULL
)
return
0
;
return
ctx
->
client
->
id
;
}
/* This is an helper for RM_GetClientInfoById() and other functions: given
* a client, it populates the client info structure with the appropriate
* fields depending on the version provided. If the version is not valid
* then REDISMODULE_ERR is returned. Otherwise the function returns
* REDISMODULE_OK and the structure pointed by 'ci' gets populated. */
int
modulePopulateClientInfoStructure
(
void
*
ci
,
client
*
client
,
int
structver
)
{
if
(
structver
!=
1
)
return
REDISMODULE_ERR
;
RedisModuleClientInfoV1
*
ci1
=
ci
;
memset
(
ci1
,
0
,
sizeof
(
*
ci1
));
ci1
->
version
=
structver
;
if
(
client
->
flags
&
CLIENT_MULTI
)
ci1
->
flags
|=
REDISMODULE_CLIENTINFO_FLAG_MULTI
;
if
(
client
->
flags
&
CLIENT_PUBSUB
)
ci1
->
flags
|=
REDISMODULE_CLIENTINFO_FLAG_PUBSUB
;
if
(
client
->
flags
&
CLIENT_UNIX_SOCKET
)
ci1
->
flags
|=
REDISMODULE_CLIENTINFO_FLAG_UNIXSOCKET
;
if
(
client
->
flags
&
CLIENT_TRACKING
)
ci1
->
flags
|=
REDISMODULE_CLIENTINFO_FLAG_TRACKING
;
if
(
client
->
flags
&
CLIENT_BLOCKED
)
ci1
->
flags
|=
REDISMODULE_CLIENTINFO_FLAG_BLOCKED
;
int
port
;
connPeerToString
(
client
->
conn
,
ci1
->
addr
,
sizeof
(
ci1
->
addr
),
&
port
);
ci1
->
port
=
port
;
ci1
->
db
=
client
->
db
->
id
;
ci1
->
id
=
client
->
id
;
return
REDISMODULE_OK
;
}
/* This is an helper for moduleFireServerEvent() and other functions:
* It populates the replication info structure with the appropriate
* fields depending on the version provided. If the version is not valid
* then REDISMODULE_ERR is returned. Otherwise the function returns
* REDISMODULE_OK and the structure pointed by 'ri' gets populated. */
int
modulePopulateReplicationInfoStructure
(
void
*
ri
,
int
structver
)
{
if
(
structver
!=
1
)
return
REDISMODULE_ERR
;
RedisModuleReplicationInfoV1
*
ri1
=
ri
;
memset
(
ri1
,
0
,
sizeof
(
*
ri1
));
ri1
->
version
=
structver
;
ri1
->
master
=
server
.
masterhost
==
NULL
;
ri1
->
masterhost
=
server
.
masterhost
?
server
.
masterhost
:
""
;
ri1
->
masterport
=
server
.
masterport
;
ri1
->
replid1
=
server
.
replid
;
ri1
->
replid2
=
server
.
replid2
;
ri1
->
repl1_offset
=
server
.
master_repl_offset
;
ri1
->
repl2_offset
=
server
.
second_replid_offset
;
return
REDISMODULE_OK
;
}
/* Return information about the client with the specified ID (that was
* previously obtained via the RedisModule_GetClientId() API). If the
* client exists, REDISMODULE_OK is returned, otherwise REDISMODULE_ERR
* is returned.
*
* When the client exist and the `ci` pointer is not NULL, but points to
* a structure of type RedisModuleClientInfo, previously initialized with
* the correct REDISMODULE_CLIENTINFO_INITIALIZER, the structure is populated
* with the following fields:
*
* uint64_t flags; // REDISMODULE_CLIENTINFO_FLAG_*
* uint64_t id; // Client ID
* char addr[46]; // IPv4 or IPv6 address.
* uint16_t port; // TCP port.
* uint16_t db; // Selected DB.
*
* Note: the client ID is useless in the context of this call, since we
* already know, however the same structure could be used in other
* contexts where we don't know the client ID, yet the same structure
* is returned.
*
* With flags having the following meaning:
*
* REDISMODULE_CLIENTINFO_FLAG_SSL Client using SSL connection.
* REDISMODULE_CLIENTINFO_FLAG_PUBSUB Client in Pub/Sub mode.
* REDISMODULE_CLIENTINFO_FLAG_BLOCKED Client blocked in command.
* REDISMODULE_CLIENTINFO_FLAG_TRACKING Client with keys tracking on.
* REDISMODULE_CLIENTINFO_FLAG_UNIXSOCKET Client using unix domain socket.
* REDISMODULE_CLIENTINFO_FLAG_MULTI Client in MULTI state.
*
* However passing NULL is a way to just check if the client exists in case
* we are not interested in any additional information.
*
* This is the correct usage when we want the client info structure
* returned:
*
* RedisModuleClientInfo ci = REDISMODULE_CLIENTINFO_INITIALIZER;
* int retval = RedisModule_GetClientInfoById(&ci,client_id);
* if (retval == REDISMODULE_OK) {
* printf("Address: %s\n", ci.addr);
* }
*/
int
RM_GetClientInfoById
(
void
*
ci
,
uint64_t
id
)
{
client
*
client
=
lookupClientByID
(
id
);
if
(
client
==
NULL
)
return
REDISMODULE_ERR
;
if
(
ci
==
NULL
)
return
REDISMODULE_OK
;
/* Fill the info structure if passed. */
uint64_t
structver
=
((
uint64_t
*
)
ci
)[
0
];
return
modulePopulateClientInfoStructure
(
ci
,
client
,
structver
);
}
/* Publish a message to subscribers (see PUBLISH command). */
int
RM_PublishMessage
(
RedisModuleCtx
*
ctx
,
RedisModuleString
*
channel
,
RedisModuleString
*
message
)
{
UNUSED
(
ctx
);
int
receivers
=
pubsubPublishMessage
(
channel
,
message
);
if
(
server
.
cluster_enabled
)
clusterPropagatePublish
(
channel
,
message
);
return
receivers
;
}
/* Return the currently selected DB. */
int
RM_GetSelectedDb
(
RedisModuleCtx
*
ctx
)
{
return
ctx
->
client
->
db
->
id
;
...
...
@@ -1449,6 +1794,23 @@ int RM_GetSelectedDb(RedisModuleCtx *ctx) {
*
* * REDISMODULE_CTX_FLAGS_OOM_WARNING: Less than 25% of memory remains before
* reaching the maxmemory level.
*
* * REDISMODULE_CTX_FLAGS_LOADING: Server is loading RDB/AOF
*
* * REDISMODULE_CTX_FLAGS_REPLICA_IS_STALE: No active link with the master.
*
* * REDISMODULE_CTX_FLAGS_REPLICA_IS_CONNECTING: The replica is trying to
* connect with the master.
*
* * REDISMODULE_CTX_FLAGS_REPLICA_IS_TRANSFERRING: Master -> Replica RDB
* transfer is in progress.
*
* * REDISMODULE_CTX_FLAGS_REPLICA_IS_ONLINE: The replica has an active link
* with its master. This is the
* contrary of STALE state.
*
* * REDISMODULE_CTX_FLAGS_ACTIVE_CHILD: There is currently some background
* process active (RDB, AUX or module).
*/
int
RM_GetContextFlags
(
RedisModuleCtx
*
ctx
)
{
...
...
@@ -1491,6 +1853,20 @@ int RM_GetContextFlags(RedisModuleCtx *ctx) {
flags
|=
REDISMODULE_CTX_FLAGS_SLAVE
;
if
(
server
.
repl_slave_ro
)
flags
|=
REDISMODULE_CTX_FLAGS_READONLY
;
/* Replica state flags. */
if
(
server
.
repl_state
==
REPL_STATE_CONNECT
||
server
.
repl_state
==
REPL_STATE_CONNECTING
)
{
flags
|=
REDISMODULE_CTX_FLAGS_REPLICA_IS_CONNECTING
;
}
else
if
(
server
.
repl_state
==
REPL_STATE_TRANSFER
)
{
flags
|=
REDISMODULE_CTX_FLAGS_REPLICA_IS_TRANSFERRING
;
}
else
if
(
server
.
repl_state
==
REPL_STATE_CONNECTED
)
{
flags
|=
REDISMODULE_CTX_FLAGS_REPLICA_IS_ONLINE
;
}
if
(
server
.
repl_state
!=
REPL_STATE_CONNECTED
)
flags
|=
REDISMODULE_CTX_FLAGS_REPLICA_IS_STALE
;
}
/* OOM flag. */
...
...
@@ -1499,6 +1875,9 @@ int RM_GetContextFlags(RedisModuleCtx *ctx) {
if
(
retval
==
C_ERR
)
flags
|=
REDISMODULE_CTX_FLAGS_OOM
;
if
(
level
>
0
.
75
)
flags
|=
REDISMODULE_CTX_FLAGS_OOM_WARNING
;
/* Presence of children processes. */
if
(
hasActiveChildProcess
())
flags
|=
REDISMODULE_CTX_FLAGS_ACTIVE_CHILD
;
return
flags
;
}
...
...
@@ -1517,6 +1896,18 @@ int RM_SelectDb(RedisModuleCtx *ctx, int newid) {
return
(
retval
==
C_OK
)
?
REDISMODULE_OK
:
REDISMODULE_ERR
;
}
/* Initialize a RedisModuleKey struct */
static
void
moduleInitKey
(
RedisModuleKey
*
kp
,
RedisModuleCtx
*
ctx
,
robj
*
keyname
,
robj
*
value
,
int
mode
){
kp
->
ctx
=
ctx
;
kp
->
db
=
ctx
->
client
->
db
;
kp
->
key
=
keyname
;
incrRefCount
(
keyname
);
kp
->
value
=
value
;
kp
->
iter
=
NULL
;
kp
->
mode
=
mode
;
zsetKeyReset
(
kp
);
}
/* Return an handle representing a Redis key, so that it is possible
* to call other APIs with the key handle as argument to perform
* operations on the key.
...
...
@@ -1534,11 +1925,12 @@ int RM_SelectDb(RedisModuleCtx *ctx, int newid) {
void
*
RM_OpenKey
(
RedisModuleCtx
*
ctx
,
robj
*
keyname
,
int
mode
)
{
RedisModuleKey
*
kp
;
robj
*
value
;
int
flags
=
mode
&
REDISMODULE_OPEN_KEY_NOTOUCH
?
LOOKUP_NOTOUCH
:
0
;
if
(
mode
&
REDISMODULE_WRITE
)
{
value
=
lookupKeyWrite
(
ctx
->
client
->
db
,
keyname
);
value
=
lookupKeyWrite
WithFlags
(
ctx
->
client
->
db
,
keyname
,
flags
);
}
else
{
value
=
lookupKeyRead
(
ctx
->
client
->
db
,
keyname
);
value
=
lookupKeyRead
WithFlags
(
ctx
->
client
->
db
,
keyname
,
flags
);
if
(
value
==
NULL
)
{
return
NULL
;
}
...
...
@@ -1546,25 +1938,25 @@ void *RM_OpenKey(RedisModuleCtx *ctx, robj *keyname, int mode) {
/* Setup the key handle. */
kp
=
zmalloc
(
sizeof
(
*
kp
));
kp
->
ctx
=
ctx
;
kp
->
db
=
ctx
->
client
->
db
;
kp
->
key
=
keyname
;
incrRefCount
(
keyname
);
kp
->
value
=
value
;
kp
->
iter
=
NULL
;
kp
->
mode
=
mode
;
zsetKeyReset
(
kp
);
moduleInitKey
(
kp
,
ctx
,
keyname
,
value
,
mode
);
autoMemoryAdd
(
ctx
,
REDISMODULE_AM_KEY
,
kp
);
return
(
void
*
)
kp
;
}
/* Close a key handle. */
void
RM_CloseKey
(
RedisModuleKey
*
key
)
{
if
(
key
==
NULL
)
return
;
if
(
key
->
mode
&
REDISMODULE_WRITE
)
signalModifiedKey
(
key
->
db
,
key
->
key
);
/* Destroy a RedisModuleKey struct (freeing is the responsibility of the caller). */
static
void
moduleCloseKey
(
RedisModuleKey
*
key
)
{
int
signal
=
SHOULD_SIGNAL_MODIFIED_KEYS
(
key
->
ctx
);
if
((
key
->
mode
&
REDISMODULE_WRITE
)
&&
signal
)
signalModifiedKey
(
key
->
db
,
key
->
key
);
/* TODO: if (key->iter) RM_KeyIteratorStop(kp); */
RM_ZsetRangeStop
(
key
);
decrRefCount
(
key
->
key
);
}
/* Close a key handle. */
void
RM_CloseKey
(
RedisModuleKey
*
key
)
{
if
(
key
==
NULL
)
return
;
moduleCloseKey
(
key
);
autoMemoryFreed
(
key
->
ctx
,
REDISMODULE_AM_KEY
,
key
);
zfree
(
key
);
}
...
...
@@ -1582,6 +1974,7 @@ int RM_KeyType(RedisModuleKey *key) {
case
OBJ_ZSET
:
return
REDISMODULE_KEYTYPE_ZSET
;
case
OBJ_HASH
:
return
REDISMODULE_KEYTYPE_HASH
;
case
OBJ_MODULE
:
return
REDISMODULE_KEYTYPE_MODULE
;
case
OBJ_STREAM
:
return
REDISMODULE_KEYTYPE_STREAM
;
default:
return
0
;
}
}
...
...
@@ -1599,6 +1992,7 @@ size_t RM_ValueLength(RedisModuleKey *key) {
case
OBJ_SET
:
return
setTypeSize
(
key
->
value
);
case
OBJ_ZSET
:
return
zsetLength
(
key
->
value
);
case
OBJ_HASH
:
return
hashTypeLength
(
key
->
value
);
case
OBJ_STREAM
:
return
streamLength
(
key
->
value
);
default:
return
0
;
}
}
...
...
@@ -1661,6 +2055,28 @@ int RM_SetExpire(RedisModuleKey *key, mstime_t expire) {
return
REDISMODULE_OK
;
}
/* Performs similar operation to FLUSHALL, and optionally start a new AOF file (if enabled)
* If restart_aof is true, you must make sure the command that triggered this call is not
* propagated to the AOF file.
* When async is set to true, db contents will be freed by a background thread. */
void
RM_ResetDataset
(
int
restart_aof
,
int
async
)
{
if
(
restart_aof
&&
server
.
aof_state
!=
AOF_OFF
)
stopAppendOnly
();
flushAllDataAndResetRDB
(
async
?
EMPTYDB_ASYNC
:
EMPTYDB_NO_FLAGS
);
if
(
server
.
aof_enabled
&&
restart_aof
)
restartAOFAfterSYNC
();
}
/* Returns the number of keys in the current db. */
unsigned
long
long
RM_DbSize
(
RedisModuleCtx
*
ctx
)
{
return
dictSize
(
ctx
->
client
->
db
->
dict
);
}
/* Returns a name of a random key, or NULL if current db is empty. */
RedisModuleString
*
RM_RandomKey
(
RedisModuleCtx
*
ctx
)
{
robj
*
key
=
dbRandomKey
(
ctx
->
client
->
db
);
autoMemoryAdd
(
ctx
,
REDISMODULE_AM_STRING
,
key
);
return
key
;
}
/* --------------------------------------------------------------------------
* Key API for String type
* -------------------------------------------------------------------------- */
...
...
@@ -2389,7 +2805,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
*
* REDISMODULE_HASH_EXISTS: instead of setting the value of the field
* expecting a RedisModuleString pointer to pointer, the function just
* reports if the field e
s
ists or not and expects an integer pointer
* reports if the field e
x
ists or not and expects an integer pointer
* as the second element of each pair.
*
* Example of REDISMODULE_HASH_CFIELD:
...
...
@@ -2678,12 +3094,11 @@ RedisModuleString *RM_CreateStringFromCallReply(RedisModuleCallReply *reply) {
* to special modifiers in "fmt". For now only one exists:
*
* "!" -> REDISMODULE_ARGV_REPLICATE
* "A" -> REDISMODULE_ARGV_NO_AOF
* "R" -> REDISMODULE_ARGV_NO_REPLICAS
*
* On error (format specifier error) NULL is returned and nothing is
* allocated. On success the argument vector is returned. */
#define REDISMODULE_ARGV_REPLICATE (1<<0)
robj
**
moduleCreateArgvFromUserFormat
(
const
char
*
cmdname
,
const
char
*
fmt
,
int
*
argcp
,
int
*
flags
,
va_list
ap
)
{
int
argc
=
0
,
argv_size
,
j
;
robj
**
argv
=
NULL
;
...
...
@@ -2712,7 +3127,7 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int
size_t
len
=
va_arg
(
ap
,
size_t
);
argv
[
argc
++
]
=
createStringObject
(
buf
,
len
);
}
else
if
(
*
p
==
'l'
)
{
long
ll
=
va_arg
(
ap
,
long
long
);
long
long
ll
=
va_arg
(
ap
,
long
long
);
argv
[
argc
++
]
=
createObject
(
OBJ_STRING
,
sdsfromlonglong
(
ll
));
}
else
if
(
*
p
==
'v'
)
{
/* A vector of strings */
...
...
@@ -2732,6 +3147,10 @@ robj **moduleCreateArgvFromUserFormat(const char *cmdname, const char *fmt, int
}
}
else
if
(
*
p
==
'!'
)
{
if
(
flags
)
(
*
flags
)
|=
REDISMODULE_ARGV_REPLICATE
;
}
else
if
(
*
p
==
'A'
)
{
if
(
flags
)
(
*
flags
)
|=
REDISMODULE_ARGV_NO_AOF
;
}
else
if
(
*
p
==
'R'
)
{
if
(
flags
)
(
*
flags
)
|=
REDISMODULE_ARGV_NO_REPLICAS
;
}
else
{
goto
fmterr
;
}
...
...
@@ -2751,8 +3170,13 @@ fmterr:
* On success a RedisModuleCallReply object is returned, otherwise
* NULL is returned and errno is set to the following values:
*
* EINVAL: command non existing, wrong arity, wrong format specifier.
* EPERM: operation in Cluster instance with key in non local slot. */
* EBADF: wrong format specifier.
* EINVAL: wrong command arity.
* ENOENT: command does not exist.
* EPERM: operation in Cluster instance with key in non local slot.
*
* This API is documented here: https://redis.io/topics/modules-intro
*/
RedisModuleCallReply
*
RM_Call
(
RedisModuleCtx
*
ctx
,
const
char
*
cmdname
,
const
char
*
fmt
,
...)
{
struct
redisCommand
*
cmd
;
client
*
c
=
NULL
;
...
...
@@ -2764,7 +3188,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
/* Create the client and dispatch the command. */
va_start
(
ap
,
fmt
);
c
=
createClient
(
-
1
);
c
=
createClient
(
NULL
);
c
->
user
=
NULL
;
/* Root user. */
argv
=
moduleCreateArgvFromUserFormat
(
cmdname
,
fmt
,
&
argc
,
&
flags
,
ap
);
replicate
=
flags
&
REDISMODULE_ARGV_REPLICATE
;
...
...
@@ -2779,7 +3203,10 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
/* We handle the above format error only when the client is setup so that
* we can free it normally. */
if
(
argv
==
NULL
)
goto
cleanup
;
if
(
argv
==
NULL
)
{
errno
=
EBADF
;
goto
cleanup
;
}
/* Call command filters */
moduleCallCommandFilters
(
c
);
...
...
@@ -2789,7 +3216,7 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
*/
cmd
=
lookupCommand
(
c
->
argv
[
0
]
->
ptr
);
if
(
!
cmd
)
{
errno
=
E
INVAL
;
errno
=
E
NOENT
;
goto
cleanup
;
}
c
->
cmd
=
c
->
lastcmd
=
cmd
;
...
...
@@ -2823,7 +3250,9 @@ RedisModuleCallReply *RM_Call(RedisModuleCtx *ctx, const char *cmdname, const ch
/* Run the command */
int
call_flags
=
CMD_CALL_SLOWLOG
|
CMD_CALL_STATS
;
if
(
replicate
)
{
if
(
!
(
flags
&
REDISMODULE_ARGV_NO_AOF
))
call_flags
|=
CMD_CALL_PROPAGATE_AOF
;
if
(
!
(
flags
&
REDISMODULE_ARGV_NO_REPLICAS
))
call_flags
|=
CMD_CALL_PROPAGATE_REPL
;
}
call
(
c
,
call_flags
);
...
...
@@ -3079,6 +3508,11 @@ moduleType *RM_CreateDataType(RedisModuleCtx *ctx, const char *name, int encver,
moduleTypeMemUsageFunc
mem_usage
;
moduleTypeDigestFunc
digest
;
moduleTypeFreeFunc
free
;
struct
{
moduleTypeAuxLoadFunc
aux_load
;
moduleTypeAuxSaveFunc
aux_save
;
int
aux_save_triggers
;
}
v2
;
}
*
tms
=
(
struct
typemethods
*
)
typemethods_ptr
;
moduleType
*
mt
=
zcalloc
(
sizeof
(
*
mt
));
...
...
@@ -3090,6 +3524,11 @@ moduleType *RM_CreateDataType(RedisModuleCtx *ctx, const char *name, int encver,
mt
->
mem_usage
=
tms
->
mem_usage
;
mt
->
digest
=
tms
->
digest
;
mt
->
free
=
tms
->
free
;
if
(
tms
->
version
>=
2
)
{
mt
->
aux_load
=
tms
->
v2
.
aux_load
;
mt
->
aux_save
=
tms
->
v2
.
aux_save
;
mt
->
aux_save_triggers
=
tms
->
v2
.
aux_save_triggers
;
}
memcpy
(
mt
->
name
,
name
,
sizeof
(
mt
->
name
));
listAddNodeTail
(
ctx
->
module
->
types
,
mt
);
return
mt
;
...
...
@@ -3140,9 +3579,14 @@ void *RM_ModuleTypeGetValue(RedisModuleKey *key) {
* RDB loading and saving functions
* -------------------------------------------------------------------------- */
/* Called when there is a load error in the context of a module. This cannot
* be recovered like for the built-in types. */
/* Called when there is a load error in the context of a module. On some
* modules this cannot be recovered, but if the module declared capability
* to handle errors, we'll raise a flag rather than exiting. */
void
moduleRDBLoadError
(
RedisModuleIO
*
io
)
{
if
(
io
->
type
->
module
->
options
&
REDISMODULE_OPTIONS_HANDLE_IO_ERRORS
)
{
io
->
error
=
1
;
return
;
}
serverLog
(
LL_WARNING
,
"Error loading data from RDB (short read or EOF). "
"Read performed by module '%s' about type '%s' "
...
...
@@ -3153,6 +3597,33 @@ void moduleRDBLoadError(RedisModuleIO *io) {
exit
(
1
);
}
/* Returns 0 if there's at least one registered data type that did not declare
* REDISMODULE_OPTIONS_HANDLE_IO_ERRORS, in which case diskless loading should
* be avoided since it could cause data loss. */
int
moduleAllDatatypesHandleErrors
()
{
dictIterator
*
di
=
dictGetIterator
(
modules
);
dictEntry
*
de
;
while
((
de
=
dictNext
(
di
))
!=
NULL
)
{
struct
RedisModule
*
module
=
dictGetVal
(
de
);
if
(
listLength
(
module
->
types
)
&&
!
(
module
->
options
&
REDISMODULE_OPTIONS_HANDLE_IO_ERRORS
))
{
dictReleaseIterator
(
di
);
return
0
;
}
}
dictReleaseIterator
(
di
);
return
1
;
}
/* Returns true if any previous IO API failed.
* for Load* APIs the REDISMODULE_OPTIONS_HANDLE_IO_ERRORS flag must be set with
* RediModule_SetModuleOptions first. */
int
RM_IsIOError
(
RedisModuleIO
*
io
)
{
return
io
->
error
;
}
/* Save an unsigned 64 bit value into the RDB file. This function should only
* be called in the context of the rdb_save method of modules implementing new
* data types. */
...
...
@@ -3176,6 +3647,7 @@ saveerr:
* be called in the context of the rdb_load method of modules implementing
* new data types. */
uint64_t
RM_LoadUnsigned
(
RedisModuleIO
*
io
)
{
if
(
io
->
error
)
return
0
;
if
(
io
->
ver
==
2
)
{
uint64_t
opcode
=
rdbLoadLen
(
io
->
rio
,
NULL
);
if
(
opcode
!=
RDB_MODULE_OPCODE_UINT
)
goto
loaderr
;
...
...
@@ -3187,7 +3659,7 @@ uint64_t RM_LoadUnsigned(RedisModuleIO *io) {
loaderr:
moduleRDBLoadError
(
io
);
return
0
;
/* Never reached. */
return
0
;
}
/* Like RedisModule_SaveUnsigned() but for signed 64 bit values. */
...
...
@@ -3246,6 +3718,7 @@ saveerr:
/* Implements RM_LoadString() and RM_LoadStringBuffer() */
void
*
moduleLoadString
(
RedisModuleIO
*
io
,
int
plain
,
size_t
*
lenptr
)
{
if
(
io
->
error
)
return
NULL
;
if
(
io
->
ver
==
2
)
{
uint64_t
opcode
=
rdbLoadLen
(
io
->
rio
,
NULL
);
if
(
opcode
!=
RDB_MODULE_OPCODE_STRING
)
goto
loaderr
;
...
...
@@ -3257,7 +3730,7 @@ void *moduleLoadString(RedisModuleIO *io, int plain, size_t *lenptr) {
loaderr:
moduleRDBLoadError
(
io
);
return
NULL
;
/* Never reached. */
return
NULL
;
}
/* In the context of the rdb_load method of a module data type, loads a string
...
...
@@ -3278,7 +3751,7 @@ RedisModuleString *RM_LoadString(RedisModuleIO *io) {
* RedisModule_Realloc() or RedisModule_Free().
*
* The size of the string is stored at '*lenptr' if not NULL.
* The returned string is not automatically NULL termi
a
nted, it is loaded
* The returned string is not automatically NULL termin
a
ted, it is loaded
* exactly as it was stored inisde the RDB file. */
char
*
RM_LoadStringBuffer
(
RedisModuleIO
*
io
,
size_t
*
lenptr
)
{
return
moduleLoadString
(
io
,
1
,
lenptr
);
...
...
@@ -3306,6 +3779,7 @@ saveerr:
/* In the context of the rdb_save method of a module data type, loads back the
* double value saved by RedisModule_SaveDouble(). */
double
RM_LoadDouble
(
RedisModuleIO
*
io
)
{
if
(
io
->
error
)
return
0
;
if
(
io
->
ver
==
2
)
{
uint64_t
opcode
=
rdbLoadLen
(
io
->
rio
,
NULL
);
if
(
opcode
!=
RDB_MODULE_OPCODE_DOUBLE
)
goto
loaderr
;
...
...
@@ -3317,7 +3791,7 @@ double RM_LoadDouble(RedisModuleIO *io) {
loaderr:
moduleRDBLoadError
(
io
);
return
0
;
/* Never reached. */
return
0
;
}
/* In the context of the rdb_save method of a module data type, saves a float
...
...
@@ -3342,6 +3816,7 @@ saveerr:
/* In the context of the rdb_save method of a module data type, loads back the
* float value saved by RedisModule_SaveFloat(). */
float
RM_LoadFloat
(
RedisModuleIO
*
io
)
{
if
(
io
->
error
)
return
0
;
if
(
io
->
ver
==
2
)
{
uint64_t
opcode
=
rdbLoadLen
(
io
->
rio
,
NULL
);
if
(
opcode
!=
RDB_MODULE_OPCODE_FLOAT
)
goto
loaderr
;
...
...
@@ -3353,7 +3828,62 @@ float RM_LoadFloat(RedisModuleIO *io) {
loaderr:
moduleRDBLoadError
(
io
);
return
0
;
/* Never reached. */
return
0
;
}
/* In the context of the rdb_save method of a module data type, saves a long double
* value to the RDB file. The double can be a valid number, a NaN or infinity.
* It is possible to load back the value with RedisModule_LoadLongDouble(). */
void
RM_SaveLongDouble
(
RedisModuleIO
*
io
,
long
double
value
)
{
if
(
io
->
error
)
return
;
char
buf
[
MAX_LONG_DOUBLE_CHARS
];
/* Long double has different number of bits in different platforms, so we
* save it as a string type. */
size_t
len
=
ld2string
(
buf
,
sizeof
(
buf
),
value
,
LD_STR_HEX
);
RM_SaveStringBuffer
(
io
,
buf
,
len
+
1
);
/* len+1 for '\0' */
}
/* In the context of the rdb_save method of a module data type, loads back the
* long double value saved by RedisModule_SaveLongDouble(). */
long
double
RM_LoadLongDouble
(
RedisModuleIO
*
io
)
{
if
(
io
->
error
)
return
0
;
long
double
value
;
size_t
len
;
char
*
str
=
RM_LoadStringBuffer
(
io
,
&
len
);
if
(
!
str
)
return
0
;
string2ld
(
str
,
len
,
&
value
);
RM_Free
(
str
);
return
value
;
}
/* Iterate over modules, and trigger rdb aux saving for the ones modules types
* who asked for it. */
ssize_t
rdbSaveModulesAux
(
rio
*
rdb
,
int
when
)
{
size_t
total_written
=
0
;
dictIterator
*
di
=
dictGetIterator
(
modules
);
dictEntry
*
de
;
while
((
de
=
dictNext
(
di
))
!=
NULL
)
{
struct
RedisModule
*
module
=
dictGetVal
(
de
);
listIter
li
;
listNode
*
ln
;
listRewind
(
module
->
types
,
&
li
);
while
((
ln
=
listNext
(
&
li
)))
{
moduleType
*
mt
=
ln
->
value
;
if
(
!
mt
->
aux_save
||
!
(
mt
->
aux_save_triggers
&
when
))
continue
;
ssize_t
ret
=
rdbSaveSingleModuleAux
(
rdb
,
when
,
mt
);
if
(
ret
==-
1
)
{
dictReleaseIterator
(
di
);
return
-
1
;
}
total_written
+=
ret
;
}
}
dictReleaseIterator
(
di
);
return
total_written
;
}
/* --------------------------------------------------------------------------
...
...
@@ -3416,9 +3946,72 @@ void RM_DigestEndSequence(RedisModuleDigest *md) {
memset
(
md
->
o
,
0
,
sizeof
(
md
->
o
));
}
/* --------------------------------------------------------------------------
* AOF API for modules data types
* -------------------------------------------------------------------------- */
/* Decode a serialized representation of a module data type 'mt' from string
* 'str' and return a newly allocated value, or NULL if decoding failed.
*
* This call basically reuses the 'rdb_load' callback which module data types
* implement in order to allow a module to arbitrarily serialize/de-serialize
* keys, similar to how the Redis 'DUMP' and 'RESTORE' commands are implemented.
*
* Modules should generally use the REDISMODULE_OPTIONS_HANDLE_IO_ERRORS flag and
* make sure the de-serialization code properly checks and handles IO errors
* (freeing allocated buffers and returning a NULL).
*
* If this is NOT done, Redis will handle corrupted (or just truncated) serialized
* data by producing an error message and terminating the process.
*/
void
*
RM_LoadDataTypeFromString
(
const
RedisModuleString
*
str
,
const
moduleType
*
mt
)
{
rio
payload
;
RedisModuleIO
io
;
void
*
ret
;
rioInitWithBuffer
(
&
payload
,
str
->
ptr
);
moduleInitIOContext
(
io
,(
moduleType
*
)
mt
,
&
payload
,
NULL
);
/* All RM_Save*() calls always write a version 2 compatible format, so we
* need to make sure we read the same.
*/
io
.
ver
=
2
;
ret
=
mt
->
rdb_load
(
&
io
,
0
);
if
(
io
.
ctx
)
{
moduleFreeContext
(
io
.
ctx
);
zfree
(
io
.
ctx
);
}
return
ret
;
}
/* Encode a module data type 'mt' value 'data' into serialized form, and return it
* as a newly allocated RedisModuleString.
*
* This call basically reuses the 'rdb_save' callback which module data types
* implement in order to allow a module to arbitrarily serialize/de-serialize
* keys, similar to how the Redis 'DUMP' and 'RESTORE' commands are implemented.
*/
RedisModuleString
*
RM_SaveDataTypeToString
(
RedisModuleCtx
*
ctx
,
void
*
data
,
const
moduleType
*
mt
)
{
rio
payload
;
RedisModuleIO
io
;
rioInitWithBuffer
(
&
payload
,
sdsempty
());
moduleInitIOContext
(
io
,(
moduleType
*
)
mt
,
&
payload
,
NULL
);
mt
->
rdb_save
(
&
io
,
data
);
if
(
io
.
ctx
)
{
moduleFreeContext
(
io
.
ctx
);
zfree
(
io
.
ctx
);
}
if
(
io
.
error
)
{
return
NULL
;
}
else
{
robj
*
str
=
createObject
(
OBJ_STRING
,
payload
.
io
.
buffer
.
ptr
);
if
(
ctx
!=
NULL
)
autoMemoryAdd
(
ctx
,
REDISMODULE_AM_STRING
,
str
);
return
str
;
}
}
/* --------------------------------------------------------------------------
* AOF API for modules data types
* -------------------------------------------------------------------------- */
/* Emits a command into the AOF during the AOF rewriting process. This function
* is only called in the context of the aof_rewrite method of data types exported
...
...
@@ -3493,6 +4086,11 @@ const RedisModuleString *RM_GetKeyNameFromIO(RedisModuleIO *io) {
return
io
->
key
;
}
/* Returns a RedisModuleString with the name of the key from RedisModuleKey */
const
RedisModuleString
*
RM_GetKeyNameFromModuleKey
(
RedisModuleKey
*
key
)
{
return
key
?
key
->
key
:
NULL
;
}
/* --------------------------------------------------------------------------
* Logging
* -------------------------------------------------------------------------- */
...
...
@@ -3516,7 +4114,7 @@ void RM_LogRaw(RedisModule *module, const char *levelstr, const char *fmt, va_li
if
(
level
<
server
.
verbosity
)
return
;
name_len
=
snprintf
(
msg
,
sizeof
(
msg
),
"<%s> "
,
module
->
name
);
name_len
=
snprintf
(
msg
,
sizeof
(
msg
),
"<%s> "
,
module
?
module
->
name
:
"module"
);
vsnprintf
(
msg
+
name_len
,
sizeof
(
msg
)
-
name_len
,
fmt
,
ap
);
serverLogRaw
(
level
,
msg
);
}
...
...
@@ -3534,13 +4132,15 @@ void RM_LogRaw(RedisModule *module, const char *levelstr, const char *fmt, va_li
* There is a fixed limit to the length of the log line this function is able
* to emit, this limit is not specified but is guaranteed to be more than
* a few lines of text.
*
* The ctx argument may be NULL if cannot be provided in the context of the
* caller for instance threads or callbacks, in which case a generic "module"
* will be used instead of the module name.
*/
void
RM_Log
(
RedisModuleCtx
*
ctx
,
const
char
*
levelstr
,
const
char
*
fmt
,
...)
{
if
(
!
ctx
->
module
)
return
;
/* Can only log if module is initialized */
va_list
ap
;
va_start
(
ap
,
fmt
);
RM_LogRaw
(
ctx
->
module
,
levelstr
,
fmt
,
ap
);
RM_LogRaw
(
ctx
?
ctx
->
module
:
NULL
,
levelstr
,
fmt
,
ap
);
va_end
(
ap
);
}
...
...
@@ -3556,6 +4156,23 @@ void RM_LogIOError(RedisModuleIO *io, const char *levelstr, const char *fmt, ...
va_end
(
ap
);
}
/* Redis-like assert function.
*
* A failed assertion will shut down the server and produce logging information
* that looks identical to information generated by Redis itself.
*/
void
RM__Assert
(
const
char
*
estr
,
const
char
*
file
,
int
line
)
{
_serverAssert
(
estr
,
file
,
line
);
}
/* Allows adding event to the latency monitor to be observed by the LATENCY
* command. The call is skipped if the latency is smaller than the configured
* latency-monitor-threshold. */
void
RM_LatencyAddSample
(
const
char
*
event
,
mstime_t
latency
)
{
if
(
latency
>=
server
.
latency_monitor_threshold
)
latencyAddSample
(
event
,
latency
);
}
/* --------------------------------------------------------------------------
* Blocking clients from modules
* -------------------------------------------------------------------------- */
...
...
@@ -3586,7 +4203,10 @@ void moduleBlockedClientPipeReadable(aeEventLoop *el, int fd, void *privdata, in
void
unblockClientFromModule
(
client
*
c
)
{
RedisModuleBlockedClient
*
bc
=
c
->
bpop
.
module_blocked_handle
;
/* Call the disconnection callback if any. */
/* Call the disconnection callback if any. Note that
* bc->disconnect_callback is set to NULL if the client gets disconnected
* by the module itself or because of a timeout, so the callback will NOT
* get called if this is not an actual disconnection event. */
if
(
bc
->
disconnect_callback
)
{
RedisModuleCtx
ctx
=
REDISMODULE_CTX_INIT
;
ctx
.
blocked_privdata
=
bc
->
privdata
;
...
...
@@ -3604,57 +4224,203 @@ void unblockClientFromModule(client *c) {
resetClient
(
c
);
}
/* Block a client in the context of a blocking command, returning an handle
* which will be used, later, in order to unblock the client with a call to
* RedisModule_UnblockClient(). The arguments specify callback functions
* and a timeout after which the client is unblocked.
*
* The callbacks are called in the following contexts:
/* Block a client in the context of a module: this function implements both
* RM_BlockClient() and RM_BlockClientOnKeys() depending on the fact the
* keys are passed or not.
*
* reply_callback: called after a successful RedisModule_UnblockClient()
* call in order to reply to the client and unblock it.
* When not blocking for keys, the keys, numkeys, and privdata parameters are
* not needed. The privdata in that case must be NULL, since later is
* RM_UnblockClient() that will provide some private data that the reply
* callback will receive.
*
* reply_timeout: called when the timeout is reached in order to send an
* error to the client.
* Instead when blocking for keys, normally RM_UnblockClient() will not be
* called (because the client will unblock when the key is modified), so
* 'privdata' should be provided in that case, so that once the client is
* unlocked and the reply callback is called, it will receive its associated
* private data.
*
* free_privdata: called in order to free the private data that is passed
* by RedisModule_UnblockClient() call.
* Even when blocking on keys, RM_UnblockClient() can be called however, but
* in that case the privdata argument is disregarded, because we pass the
* reply callback the privdata that is set here while blocking.
*/
RedisModuleBlockedClient
*
RM_
BlockClient
(
RedisModuleCtx
*
ctx
,
RedisModuleCmdFunc
reply_callback
,
RedisModuleCmdFunc
timeout_callback
,
void
(
*
free_privdata
)(
RedisModuleCtx
*
,
void
*
),
long
long
timeout_ms
)
{
RedisModuleBlockedClient
*
module
BlockClient
(
RedisModuleCtx
*
ctx
,
RedisModuleCmdFunc
reply_callback
,
RedisModuleCmdFunc
timeout_callback
,
void
(
*
free_privdata
)(
RedisModuleCtx
*
,
void
*
),
long
long
timeout_ms
,
RedisModuleString
**
keys
,
int
numkeys
,
void
*
privdata
)
{
client
*
c
=
ctx
->
client
;
int
islua
=
c
->
flags
&
CLIENT_LUA
;
int
ismulti
=
c
->
flags
&
CLIENT_MULTI
;
c
->
bpop
.
module_blocked_handle
=
zmalloc
(
sizeof
(
RedisModuleBlockedClient
));
RedisModuleBlockedClient
*
bc
=
c
->
bpop
.
module_blocked_handle
;
ctx
->
module
->
blocked_clients
++
;
/* We need to handle the invalid operation of calling modules blocking
* commands from Lua or MULTI. We actually create an already aborted
* (client set to NULL) blocked client handle, and actually reply with
* an error. */
mstime_t
timeout
=
timeout_ms
?
(
mstime
()
+
timeout_ms
)
:
0
;
bc
->
client
=
(
islua
||
ismulti
)
?
NULL
:
c
;
bc
->
module
=
ctx
->
module
;
bc
->
reply_callback
=
reply_callback
;
bc
->
timeout_callback
=
timeout_callback
;
bc
->
disconnect_callback
=
NULL
;
/* Set by RM_SetDisconnectCallback() */
bc
->
free_privdata
=
free_privdata
;
bc
->
privdata
=
NULL
;
bc
->
reply_client
=
createClient
(
-
1
);
bc
->
privdata
=
privdata
;
bc
->
reply_client
=
createClient
(
NULL
);
bc
->
reply_client
->
flags
|=
CLIENT_MODULE
;
bc
->
dbid
=
c
->
db
->
id
;
c
->
bpop
.
timeout
=
timeout_ms
?
(
mstime
()
+
timeout_ms
)
:
0
;
bc
->
blocked_on_keys
=
keys
!=
NULL
;
bc
->
unblocked
=
0
;
c
->
bpop
.
timeout
=
timeout
;
if
(
islua
||
ismulti
)
{
c
->
bpop
.
module_blocked_handle
=
NULL
;
addReplyError
(
c
,
islua
?
"Blocking module command called from Lua script"
:
"Blocking module command called from transaction"
);
}
else
{
if
(
keys
)
{
blockForKeys
(
c
,
BLOCKED_MODULE
,
keys
,
numkeys
,
timeout
,
NULL
,
NULL
);
}
else
{
blockClient
(
c
,
BLOCKED_MODULE
);
}
}
return
bc
;
}
/* This function is called from module.c in order to check if a module
* blocked for BLOCKED_MODULE and subtype 'on keys' (bc->blocked_on_keys true)
* can really be unblocked, since the module was able to serve the client.
* If the callback returns REDISMODULE_OK, then the client can be unblocked,
* otherwise the client remains blocked and we'll retry again when one of
* the keys it blocked for becomes "ready" again. */
int
moduleTryServeClientBlockedOnKey
(
client
*
c
,
robj
*
key
)
{
int
served
=
0
;
RedisModuleBlockedClient
*
bc
=
c
->
bpop
.
module_blocked_handle
;
/* Protect against re-processing: don't serve clients that are already
* in the unblocking list for any reason (including RM_UnblockClient()
* explicit call). */
if
(
bc
->
unblocked
)
return
REDISMODULE_ERR
;
RedisModuleCtx
ctx
=
REDISMODULE_CTX_INIT
;
ctx
.
flags
|=
REDISMODULE_CTX_BLOCKED_REPLY
;
ctx
.
blocked_ready_key
=
key
;
ctx
.
blocked_privdata
=
bc
->
privdata
;
ctx
.
module
=
bc
->
module
;
ctx
.
client
=
bc
->
client
;
ctx
.
blocked_client
=
bc
;
if
(
bc
->
reply_callback
(
&
ctx
,(
void
**
)
c
->
argv
,
c
->
argc
)
==
REDISMODULE_OK
)
served
=
1
;
moduleFreeContext
(
&
ctx
);
return
served
;
}
/* Block a client in the context of a blocking command, returning an handle
* which will be used, later, in order to unblock the client with a call to
* RedisModule_UnblockClient(). The arguments specify callback functions
* and a timeout after which the client is unblocked.
*
* The callbacks are called in the following contexts:
*
* reply_callback: called after a successful RedisModule_UnblockClient()
* call in order to reply to the client and unblock it.
*
* reply_timeout: called when the timeout is reached in order to send an
* error to the client.
*
* free_privdata: called in order to free the private data that is passed
* by RedisModule_UnblockClient() call.
*/
RedisModuleBlockedClient
*
RM_BlockClient
(
RedisModuleCtx
*
ctx
,
RedisModuleCmdFunc
reply_callback
,
RedisModuleCmdFunc
timeout_callback
,
void
(
*
free_privdata
)(
RedisModuleCtx
*
,
void
*
),
long
long
timeout_ms
)
{
return
moduleBlockClient
(
ctx
,
reply_callback
,
timeout_callback
,
free_privdata
,
timeout_ms
,
NULL
,
0
,
NULL
);
}
/* This call is similar to RedisModule_BlockClient(), however in this case we
* don't just block the client, but also ask Redis to unblock it automatically
* once certain keys become "ready", that is, contain more data.
*
* Basically this is similar to what a typical Redis command usually does,
* like BLPOP or ZPOPMAX: the client blocks if it cannot be served ASAP,
* and later when the key receives new data (a list push for instance), the
* client is unblocked and served.
*
* However in the case of this module API, when the client is unblocked?
*
* 1. If you block ok a key of a type that has blocking operations associated,
* like a list, a sorted set, a stream, and so forth, the client may be
* unblocked once the relevant key is targeted by an operation that normally
* unblocks the native blocking operations for that type. So if we block
* on a list key, an RPUSH command may unblock our client and so forth.
* 2. If you are implementing your native data type, or if you want to add new
* unblocking conditions in addition to "1", you can call the modules API
* RedisModule_SignalKeyAsReady().
*
* Anyway we can't be sure if the client should be unblocked just because the
* key is signaled as ready: for instance a successive operation may change the
* key, or a client in queue before this one can be served, modifying the key
* as well and making it empty again. So when a client is blocked with
* RedisModule_BlockClientOnKeys() the reply callback is not called after
* RM_UnblockCLient() is called, but every time a key is signaled as ready:
* if the reply callback can serve the client, it returns REDISMODULE_OK
* and the client is unblocked, otherwise it will return REDISMODULE_ERR
* and we'll try again later.
*
* The reply callback can access the key that was signaled as ready by
* calling the API RedisModule_GetBlockedClientReadyKey(), that returns
* just the string name of the key as a RedisModuleString object.
*
* Thanks to this system we can setup complex blocking scenarios, like
* unblocking a client only if a list contains at least 5 items or other
* more fancy logics.
*
* Note that another difference with RedisModule_BlockClient(), is that here
* we pass the private data directly when blocking the client: it will
* be accessible later in the reply callback. Normally when blocking with
* RedisModule_BlockClient() the private data to reply to the client is
* passed when calling RedisModule_UnblockClient() but here the unblocking
* is performed by Redis itself, so we need to have some private data before
* hand. The private data is used to store any information about the specific
* unblocking operation that you are implementing. Such information will be
* freed using the free_privdata callback provided by the user.
*
* However the reply callback will be able to access the argument vector of
* the command, so the private data is often not needed. */
RedisModuleBlockedClient
*
RM_BlockClientOnKeys
(
RedisModuleCtx
*
ctx
,
RedisModuleCmdFunc
reply_callback
,
RedisModuleCmdFunc
timeout_callback
,
void
(
*
free_privdata
)(
RedisModuleCtx
*
,
void
*
),
long
long
timeout_ms
,
RedisModuleString
**
keys
,
int
numkeys
,
void
*
privdata
)
{
return
moduleBlockClient
(
ctx
,
reply_callback
,
timeout_callback
,
free_privdata
,
timeout_ms
,
keys
,
numkeys
,
privdata
);
}
/* This function is used in order to potentially unblock a client blocked
* on keys with RedisModule_BlockClientOnKeys(). When this function is called,
* all the clients blocked for this key will get their reply callback called,
* and if the callback returns REDISMODULE_OK the client will be unblocked. */
void
RM_SignalKeyAsReady
(
RedisModuleCtx
*
ctx
,
RedisModuleString
*
key
)
{
signalKeyAsReady
(
ctx
->
client
->
db
,
key
);
}
/* Implements RM_UnblockClient() and moduleUnblockClient(). */
int
moduleUnblockClientByHandle
(
RedisModuleBlockedClient
*
bc
,
void
*
privdata
)
{
pthread_mutex_lock
(
&
moduleUnblockedClientsMutex
);
if
(
!
bc
->
blocked_on_keys
)
bc
->
privdata
=
privdata
;
bc
->
unblocked
=
1
;
listAddNodeTail
(
moduleUnblockedClients
,
bc
);
if
(
write
(
server
.
module_blocked_pipe
[
1
],
"A"
,
1
)
!=
1
)
{
/* Ignore the error, this is best-effort. */
}
pthread_mutex_unlock
(
&
moduleUnblockedClientsMutex
);
return
REDISMODULE_OK
;
}
/* This API is used by the Redis core to unblock a client that was blocked
* by a module. */
void
moduleUnblockClient
(
client
*
c
)
{
RedisModuleBlockedClient
*
bc
=
c
->
bpop
.
module_blocked_handle
;
moduleUnblockClientByHandle
(
bc
,
NULL
);
}
/* Return true if the client 'c' was blocked by a module using
* RM_BlockClientOnKeys(). */
int
moduleClientIsBlockedOnKeys
(
client
*
c
)
{
RedisModuleBlockedClient
*
bc
=
c
->
bpop
.
module_blocked_handle
;
return
bc
->
blocked_on_keys
;
}
/* Unblock a client blocked by `RedisModule_BlockedClient`. This will trigger
* the reply callbacks to be called in order to reply to the client.
* The 'privdata' argument will be accessible by the reply callback, so
...
...
@@ -3665,15 +4431,25 @@ RedisModuleBlockedClient *RM_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc
* needs to be passed to the client, included but not limited some slow
* to compute reply or some reply obtained via networking.
*
* Note: this function can be called from threads spawned by the module. */
* Note 1: this function can be called from threads spawned by the module.
*
* Note 2: when we unblock a client that is blocked for keys using
* the API RedisModule_BlockClientOnKeys(), the privdata argument here is
* not used, and the reply callback is called with the privdata pointer that
* was passed when blocking the client.
*
* Unblocking a client that was blocked for keys using this API will still
* require the client to get some reply, so the function will use the
* "timeout" handler in order to do so. */
int
RM_UnblockClient
(
RedisModuleBlockedClient
*
bc
,
void
*
privdata
)
{
pthread_mutex_lock
(
&
moduleUnblockedClientsMutex
);
bc
->
privdata
=
privdata
;
listAddNodeTail
(
moduleUnblockedClients
,
bc
);
if
(
write
(
server
.
module_blocked_pipe
[
1
],
"A"
,
1
)
!=
1
)
{
/* Ignore the error, this is best-effort. */
}
pthread_mutex_unlock
(
&
moduleUnblockedClientsMutex
);
if
(
bc
->
blocked_on_keys
)
{
/* In theory the user should always pass the timeout handler as an
* argument, but better to be safe than sorry. */
if
(
bc
->
timeout_callback
==
NULL
)
return
REDISMODULE_ERR
;
if
(
bc
->
unblocked
)
return
REDISMODULE_OK
;
if
(
bc
->
client
)
moduleBlockedClientTimedOut
(
bc
->
client
);
}
moduleUnblockClientByHandle
(
bc
,
privdata
);
return
REDISMODULE_OK
;
}
...
...
@@ -3733,16 +4509,19 @@ void moduleHandleBlockedClients(void) {
* touch the shared list. */
/* Call the reply callback if the client is valid and we have
* any callback. */
if
(
c
&&
bc
->
reply_callback
)
{
* any callback. However the callback is not called if the client
* was blocked on keys (RM_BlockClientOnKeys()), because we already
* called such callback in moduleTryServeClientBlockedOnKey() when
* the key was signaled as ready. */
if
(
c
&&
!
bc
->
blocked_on_keys
&&
bc
->
reply_callback
)
{
RedisModuleCtx
ctx
=
REDISMODULE_CTX_INIT
;
ctx
.
flags
|=
REDISMODULE_CTX_BLOCKED_REPLY
;
ctx
.
blocked_privdata
=
bc
->
privdata
;
ctx
.
blocked_ready_key
=
NULL
;
ctx
.
module
=
bc
->
module
;
ctx
.
client
=
bc
->
client
;
ctx
.
blocked_client
=
bc
;
bc
->
reply_callback
(
&
ctx
,(
void
**
)
c
->
argv
,
c
->
argc
);
moduleHandlePropagationAfterCommandCallback
(
&
ctx
);
moduleFreeContext
(
&
ctx
);
}
...
...
@@ -3785,6 +4564,7 @@ void moduleHandleBlockedClients(void) {
/* Free 'bc' only after unblocking the client, since it is
* referenced in the client blocking context, and must be valid
* when calling unblockClient(). */
bc
->
module
->
blocked_clients
--
;
zfree
(
bc
);
/* Lock again before to iterate the loop. */
...
...
@@ -3829,6 +4609,12 @@ void *RM_GetBlockedClientPrivateData(RedisModuleCtx *ctx) {
return
ctx
->
blocked_privdata
;
}
/* Get the key that is ready when the reply callback is called in the context
* of a client blocked by RedisModule_BlockClientOnKeys(). */
RedisModuleString
*
RM_GetBlockedClientReadyKey
(
RedisModuleCtx
*
ctx
)
{
return
ctx
->
blocked_ready_key
;
}
/* Get the blocked client associated with a given context.
* This is useful in the reply and timeout callbacks of blocked clients,
* before sometimes the module has the blocked client handle references
...
...
@@ -3880,10 +4666,10 @@ RedisModuleCtx *RM_GetThreadSafeContext(RedisModuleBlockedClient *bc) {
* access it safely from another thread, so we create a fake client here
* in order to keep things like the currently selected database and similar
* things. */
ctx
->
client
=
createClient
(
-
1
);
ctx
->
client
=
createClient
(
NULL
);
if
(
bc
)
{
selectDb
(
ctx
->
client
,
bc
->
dbid
);
ctx
->
client
->
id
=
bc
->
client
->
id
;
if
(
bc
->
client
)
ctx
->
client
->
id
=
bc
->
client
->
id
;
}
return
ctx
;
}
...
...
@@ -3981,6 +4767,20 @@ int RM_SubscribeToKeyspaceEvents(RedisModuleCtx *ctx, int types, RedisModuleNoti
return
REDISMODULE_OK
;
}
/* Get the configured bitmap of notify-keyspace-events (Could be used
* for additional filtering in RedisModuleNotificationFunc) */
int
RM_GetNotifyKeyspaceEvents
()
{
return
server
.
notify_keyspace_events
;
}
/* Expose notifyKeyspaceEvent to modules */
int
RM_NotifyKeyspaceEvent
(
RedisModuleCtx
*
ctx
,
int
type
,
const
char
*
event
,
RedisModuleString
*
key
)
{
if
(
!
ctx
||
!
ctx
->
client
)
return
REDISMODULE_ERR
;
notifyKeyspaceEvent
(
type
,
(
char
*
)
event
,
key
,
ctx
->
client
->
db
->
id
);
return
REDISMODULE_OK
;
}
/* Dispatcher for keyspace notifications to module subscriber functions.
* This gets called only if at least one module requested to be notified on
* keyspace notifications */
...
...
@@ -4364,7 +5164,7 @@ RedisModuleTimerID RM_CreateTimer(RedisModuleCtx *ctx, mstime_t period, RedisMod
timer
->
module
=
ctx
->
module
;
timer
->
callback
=
callback
;
timer
->
data
=
data
;
timer
->
dbid
=
ctx
->
client
->
db
->
id
;
timer
->
dbid
=
ctx
->
client
?
ctx
->
client
->
db
->
id
:
0
;
uint64_t
expiretime
=
ustime
()
+
period
*
1000
;
uint64_t
key
;
...
...
@@ -4687,6 +5487,314 @@ int RM_DictCompare(RedisModuleDictIter *di, const char *op, RedisModuleString *k
return
res
?
REDISMODULE_OK
:
REDISMODULE_ERR
;
}
/* --------------------------------------------------------------------------
* Modules Info fields
* -------------------------------------------------------------------------- */
int
RM_InfoEndDictField
(
RedisModuleInfoCtx
*
ctx
);
/* Used to start a new section, before adding any fields. the section name will
* be prefixed by "<modulename>_" and must only include A-Z,a-z,0-9.
* NULL or empty string indicates the default section (only <modulename>) is used.
* When return value is REDISMODULE_ERR, the section should and will be skipped. */
int
RM_InfoAddSection
(
RedisModuleInfoCtx
*
ctx
,
char
*
name
)
{
sds
full_name
=
sdsdup
(
ctx
->
module
->
name
);
if
(
name
!=
NULL
&&
strlen
(
name
)
>
0
)
full_name
=
sdscatfmt
(
full_name
,
"_%s"
,
name
);
/* Implicitly end dicts, instead of returning an error which is likely un checked. */
if
(
ctx
->
in_dict_field
)
RM_InfoEndDictField
(
ctx
);
/* proceed only if:
* 1) no section was requested (emit all)
* 2) the module name was requested (emit all)
* 3) this specific section was requested. */
if
(
ctx
->
requested_section
)
{
if
(
strcasecmp
(
ctx
->
requested_section
,
full_name
)
&&
strcasecmp
(
ctx
->
requested_section
,
ctx
->
module
->
name
))
{
sdsfree
(
full_name
);
ctx
->
in_section
=
0
;
return
REDISMODULE_ERR
;
}
}
if
(
ctx
->
sections
++
)
ctx
->
info
=
sdscat
(
ctx
->
info
,
"
\r\n
"
);
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"# %S
\r\n
"
,
full_name
);
ctx
->
in_section
=
1
;
sdsfree
(
full_name
);
return
REDISMODULE_OK
;
}
/* Starts a dict field, similar to the ones in INFO KEYSPACE. Use normal
* RedisModule_InfoAddField* functions to add the items to this field, and
* terminate with RedisModule_InfoEndDictField. */
int
RM_InfoBeginDictField
(
RedisModuleInfoCtx
*
ctx
,
char
*
name
)
{
if
(
!
ctx
->
in_section
)
return
REDISMODULE_ERR
;
/* Implicitly end dicts, instead of returning an error which is likely un checked. */
if
(
ctx
->
in_dict_field
)
RM_InfoEndDictField
(
ctx
);
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s_%s:"
,
ctx
->
module
->
name
,
name
);
ctx
->
in_dict_field
=
1
;
return
REDISMODULE_OK
;
}
/* Ends a dict field, see RedisModule_InfoBeginDictField */
int
RM_InfoEndDictField
(
RedisModuleInfoCtx
*
ctx
)
{
if
(
!
ctx
->
in_dict_field
)
return
REDISMODULE_ERR
;
/* trim the last ',' if found. */
if
(
ctx
->
info
[
sdslen
(
ctx
->
info
)
-
1
]
==
','
)
sdsIncrLen
(
ctx
->
info
,
-
1
);
ctx
->
info
=
sdscat
(
ctx
->
info
,
"
\r\n
"
);
ctx
->
in_dict_field
=
0
;
return
REDISMODULE_OK
;
}
/* Used by RedisModuleInfoFunc to add info fields.
* Each field will be automatically prefixed by "<modulename>_".
* Field names or values must not include \r\n of ":" */
int
RM_InfoAddFieldString
(
RedisModuleInfoCtx
*
ctx
,
char
*
field
,
RedisModuleString
*
value
)
{
if
(
!
ctx
->
in_section
)
return
REDISMODULE_ERR
;
if
(
ctx
->
in_dict_field
)
{
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s=%S,"
,
field
,
(
sds
)
value
->
ptr
);
return
REDISMODULE_OK
;
}
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s_%s:%S
\r\n
"
,
ctx
->
module
->
name
,
field
,
(
sds
)
value
->
ptr
);
return
REDISMODULE_OK
;
}
int
RM_InfoAddFieldCString
(
RedisModuleInfoCtx
*
ctx
,
char
*
field
,
char
*
value
)
{
if
(
!
ctx
->
in_section
)
return
REDISMODULE_ERR
;
if
(
ctx
->
in_dict_field
)
{
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s=%s,"
,
field
,
value
);
return
REDISMODULE_OK
;
}
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s_%s:%s
\r\n
"
,
ctx
->
module
->
name
,
field
,
value
);
return
REDISMODULE_OK
;
}
int
RM_InfoAddFieldDouble
(
RedisModuleInfoCtx
*
ctx
,
char
*
field
,
double
value
)
{
if
(
!
ctx
->
in_section
)
return
REDISMODULE_ERR
;
if
(
ctx
->
in_dict_field
)
{
ctx
->
info
=
sdscatprintf
(
ctx
->
info
,
"%s=%.17g,"
,
field
,
value
);
return
REDISMODULE_OK
;
}
ctx
->
info
=
sdscatprintf
(
ctx
->
info
,
"%s_%s:%.17g
\r\n
"
,
ctx
->
module
->
name
,
field
,
value
);
return
REDISMODULE_OK
;
}
int
RM_InfoAddFieldLongLong
(
RedisModuleInfoCtx
*
ctx
,
char
*
field
,
long
long
value
)
{
if
(
!
ctx
->
in_section
)
return
REDISMODULE_ERR
;
if
(
ctx
->
in_dict_field
)
{
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s=%I,"
,
field
,
value
);
return
REDISMODULE_OK
;
}
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s_%s:%I
\r\n
"
,
ctx
->
module
->
name
,
field
,
value
);
return
REDISMODULE_OK
;
}
int
RM_InfoAddFieldULongLong
(
RedisModuleInfoCtx
*
ctx
,
char
*
field
,
unsigned
long
long
value
)
{
if
(
!
ctx
->
in_section
)
return
REDISMODULE_ERR
;
if
(
ctx
->
in_dict_field
)
{
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s=%U,"
,
field
,
value
);
return
REDISMODULE_OK
;
}
ctx
->
info
=
sdscatfmt
(
ctx
->
info
,
"%s_%s:%U
\r\n
"
,
ctx
->
module
->
name
,
field
,
value
);
return
REDISMODULE_OK
;
}
int
RM_RegisterInfoFunc
(
RedisModuleCtx
*
ctx
,
RedisModuleInfoFunc
cb
)
{
ctx
->
module
->
info_cb
=
cb
;
return
REDISMODULE_OK
;
}
sds
modulesCollectInfo
(
sds
info
,
const
char
*
section
,
int
for_crash_report
,
int
sections
)
{
dictIterator
*
di
=
dictGetIterator
(
modules
);
dictEntry
*
de
;
while
((
de
=
dictNext
(
di
))
!=
NULL
)
{
struct
RedisModule
*
module
=
dictGetVal
(
de
);
if
(
!
module
->
info_cb
)
continue
;
RedisModuleInfoCtx
info_ctx
=
{
module
,
section
,
info
,
sections
,
0
};
module
->
info_cb
(
&
info_ctx
,
for_crash_report
);
/* Implicitly end dicts (no way to handle errors, and we must add the newline). */
if
(
info_ctx
.
in_dict_field
)
RM_InfoEndDictField
(
&
info_ctx
);
info
=
info_ctx
.
info
;
sections
=
info_ctx
.
sections
;
}
dictReleaseIterator
(
di
);
return
info
;
}
/* Get information about the server similar to the one that returns from the
* INFO command. This function takes an optional 'section' argument that may
* be NULL. The return value holds the output and can be used with
* RedisModule_ServerInfoGetField and alike to get the individual fields.
* When done, it needs to be freed with RedisModule_FreeServerInfo or with the
* automatic memory management mechanism if enabled. */
RedisModuleServerInfoData
*
RM_GetServerInfo
(
RedisModuleCtx
*
ctx
,
const
char
*
section
)
{
struct
RedisModuleServerInfoData
*
d
=
zmalloc
(
sizeof
(
*
d
));
d
->
rax
=
raxNew
();
if
(
ctx
!=
NULL
)
autoMemoryAdd
(
ctx
,
REDISMODULE_AM_INFO
,
d
);
sds
info
=
genRedisInfoString
(
section
);
int
totlines
,
i
;
sds
*
lines
=
sdssplitlen
(
info
,
sdslen
(
info
),
"
\r\n
"
,
2
,
&
totlines
);
for
(
i
=
0
;
i
<
totlines
;
i
++
)
{
sds
line
=
lines
[
i
];
if
(
line
[
0
]
==
'#'
)
continue
;
char
*
sep
=
strchr
(
line
,
':'
);
if
(
!
sep
)
continue
;
unsigned
char
*
key
=
(
unsigned
char
*
)
line
;
size_t
keylen
=
(
intptr_t
)
sep
-
(
intptr_t
)
line
;
sds
val
=
sdsnewlen
(
sep
+
1
,
sdslen
(
line
)
-
((
intptr_t
)
sep
-
(
intptr_t
)
line
)
-
1
);
if
(
!
raxTryInsert
(
d
->
rax
,
key
,
keylen
,
val
,
NULL
))
sdsfree
(
val
);
}
sdsfree
(
info
);
sdsfreesplitres
(
lines
,
totlines
);
return
d
;
}
/* Free data created with RM_GetServerInfo(). You need to pass the
* context pointer 'ctx' only if the dictionary was created using the
* context instead of passing NULL. */
void
RM_FreeServerInfo
(
RedisModuleCtx
*
ctx
,
RedisModuleServerInfoData
*
data
)
{
if
(
ctx
!=
NULL
)
autoMemoryFreed
(
ctx
,
REDISMODULE_AM_INFO
,
data
);
raxIterator
ri
;
raxStart
(
&
ri
,
data
->
rax
);
while
(
1
)
{
raxSeek
(
&
ri
,
"^"
,
NULL
,
0
);
if
(
!
raxNext
(
&
ri
))
break
;
raxRemove
(
data
->
rax
,(
unsigned
char
*
)
ri
.
key
,
ri
.
key_len
,
NULL
);
sdsfree
(
ri
.
data
);
}
raxStop
(
&
ri
);
raxFree
(
data
->
rax
);
zfree
(
data
);
}
/* Get the value of a field from data collected with RM_GetServerInfo(). You
* need to pass the context pointer 'ctx' only if you want to use auto memory
* mechanism to release the returned string. Return value will be NULL if the
* field was not found. */
RedisModuleString
*
RM_ServerInfoGetField
(
RedisModuleCtx
*
ctx
,
RedisModuleServerInfoData
*
data
,
const
char
*
field
)
{
sds
val
=
raxFind
(
data
->
rax
,
(
unsigned
char
*
)
field
,
strlen
(
field
));
if
(
val
==
raxNotFound
)
return
NULL
;
RedisModuleString
*
o
=
createStringObject
(
val
,
sdslen
(
val
));
if
(
ctx
!=
NULL
)
autoMemoryAdd
(
ctx
,
REDISMODULE_AM_STRING
,
o
);
return
o
;
}
/* Similar to RM_ServerInfoGetField, but returns a char* which should not be freed but the caller. */
const
char
*
RM_ServerInfoGetFieldC
(
RedisModuleServerInfoData
*
data
,
const
char
*
field
)
{
sds
val
=
raxFind
(
data
->
rax
,
(
unsigned
char
*
)
field
,
strlen
(
field
));
if
(
val
==
raxNotFound
)
return
NULL
;
return
val
;
}
/* Get the value of a field from data collected with RM_GetServerInfo(). If the
* field is not found, or is not numerical or out of range, return value will be
* 0, and the optional out_err argument will be set to REDISMODULE_ERR. */
long
long
RM_ServerInfoGetFieldSigned
(
RedisModuleServerInfoData
*
data
,
const
char
*
field
,
int
*
out_err
)
{
long
long
ll
;
sds
val
=
raxFind
(
data
->
rax
,
(
unsigned
char
*
)
field
,
strlen
(
field
));
if
(
val
==
raxNotFound
)
{
if
(
out_err
)
*
out_err
=
REDISMODULE_ERR
;
return
0
;
}
if
(
!
string2ll
(
val
,
sdslen
(
val
),
&
ll
))
{
if
(
out_err
)
*
out_err
=
REDISMODULE_ERR
;
return
0
;
}
if
(
out_err
)
*
out_err
=
REDISMODULE_OK
;
return
ll
;
}
/* Get the value of a field from data collected with RM_GetServerInfo(). If the
* field is not found, or is not numerical or out of range, return value will be
* 0, and the optional out_err argument will be set to REDISMODULE_ERR. */
unsigned
long
long
RM_ServerInfoGetFieldUnsigned
(
RedisModuleServerInfoData
*
data
,
const
char
*
field
,
int
*
out_err
)
{
unsigned
long
long
ll
;
sds
val
=
raxFind
(
data
->
rax
,
(
unsigned
char
*
)
field
,
strlen
(
field
));
if
(
val
==
raxNotFound
)
{
if
(
out_err
)
*
out_err
=
REDISMODULE_ERR
;
return
0
;
}
if
(
!
string2ull
(
val
,
&
ll
))
{
if
(
out_err
)
*
out_err
=
REDISMODULE_ERR
;
return
0
;
}
if
(
out_err
)
*
out_err
=
REDISMODULE_OK
;
return
ll
;
}
/* Get the value of a field from data collected with RM_GetServerInfo(). If the
* field is not found, or is not a double, return value will be 0, and the
* optional out_err argument will be set to REDISMODULE_ERR. */
double
RM_ServerInfoGetFieldDouble
(
RedisModuleServerInfoData
*
data
,
const
char
*
field
,
int
*
out_err
)
{
double
dbl
;
sds
val
=
raxFind
(
data
->
rax
,
(
unsigned
char
*
)
field
,
strlen
(
field
));
if
(
val
==
raxNotFound
)
{
if
(
out_err
)
*
out_err
=
REDISMODULE_ERR
;
return
0
;
}
if
(
!
string2d
(
val
,
sdslen
(
val
),
&
dbl
))
{
if
(
out_err
)
*
out_err
=
REDISMODULE_ERR
;
return
0
;
}
if
(
out_err
)
*
out_err
=
REDISMODULE_OK
;
return
dbl
;
}
/* --------------------------------------------------------------------------
* Modules utility APIs
* -------------------------------------------------------------------------- */
...
...
@@ -4926,6 +6034,8 @@ int RM_UnregisterCommandFilter(RedisModuleCtx *ctx, RedisModuleCommandFilter *fi
if
(
!
ln
)
return
REDISMODULE_ERR
;
/* Shouldn't happen */
listDelNode
(
ctx
->
module
->
filters
,
ln
);
zfree
(
filter
);
return
REDISMODULE_OK
;
}
...
...
@@ -5029,6 +6139,716 @@ int RM_CommandFilterArgDelete(RedisModuleCommandFilterCtx *fctx, int pos)
return
REDISMODULE_OK
;
}
/* For a given pointer allocated via RedisModule_Alloc() or
* RedisModule_Realloc(), return the amount of memory allocated for it.
* Note that this may be different (larger) than the memory we allocated
* with the allocation calls, since sometimes the underlying allocator
* will allocate more memory.
*/
size_t
RM_MallocSize
(
void
*
ptr
){
return
zmalloc_size
(
ptr
);
}
/* Return the a number between 0 to 1 indicating the amount of memory
* currently used, relative to the Redis "maxmemory" configuration.
*
* 0 - No memory limit configured.
* Between 0 and 1 - The percentage of the memory used normalized in 0-1 range.
* Exactly 1 - Memory limit reached.
* Greater 1 - More memory used than the configured limit.
*/
float
RM_GetUsedMemoryRatio
(){
float
level
;
getMaxmemoryState
(
NULL
,
NULL
,
NULL
,
&
level
);
return
level
;
}
/* --------------------------------------------------------------------------
* Scanning keyspace and hashes
* -------------------------------------------------------------------------- */
typedef
void
(
*
RedisModuleScanCB
)(
RedisModuleCtx
*
ctx
,
RedisModuleString
*
keyname
,
RedisModuleKey
*
key
,
void
*
privdata
);
typedef
struct
{
RedisModuleCtx
*
ctx
;
void
*
user_data
;
RedisModuleScanCB
fn
;
}
ScanCBData
;
typedef
struct
RedisModuleScanCursor
{
int
cursor
;
int
done
;
}
RedisModuleScanCursor
;
static
void
moduleScanCallback
(
void
*
privdata
,
const
dictEntry
*
de
)
{
ScanCBData
*
data
=
privdata
;
sds
key
=
dictGetKey
(
de
);
robj
*
val
=
dictGetVal
(
de
);
RedisModuleString
*
keyname
=
createObject
(
OBJ_STRING
,
sdsdup
(
key
));
/* Setup the key handle. */
RedisModuleKey
kp
=
{
0
};
moduleInitKey
(
&
kp
,
data
->
ctx
,
keyname
,
val
,
REDISMODULE_READ
);
data
->
fn
(
data
->
ctx
,
keyname
,
&
kp
,
data
->
user_data
);
moduleCloseKey
(
&
kp
);
decrRefCount
(
keyname
);
}
/* Create a new cursor to be used with RedisModule_Scan */
RedisModuleScanCursor
*
RM_ScanCursorCreate
()
{
RedisModuleScanCursor
*
cursor
=
zmalloc
(
sizeof
(
*
cursor
));
cursor
->
cursor
=
0
;
cursor
->
done
=
0
;
return
cursor
;
}
/* Restart an existing cursor. The keys will be rescanned. */
void
RM_ScanCursorRestart
(
RedisModuleScanCursor
*
cursor
)
{
cursor
->
cursor
=
0
;
cursor
->
done
=
0
;
}
/* Destroy the cursor struct. */
void
RM_ScanCursorDestroy
(
RedisModuleScanCursor
*
cursor
)
{
zfree
(
cursor
);
}
/* Scan api that allows a module to scan all the keys and value in the selected db.
*
* Callback for scan implementation.
* void scan_callback(RedisModuleCtx *ctx, RedisModuleString *keyname, RedisModuleKey *key, void *privdata);
* - ctx - the redis module context provided to for the scan.
* - keyname - owned by the caller and need to be retained if used after this function.
* - key - holds info on the key and value, it is provided as best effort, in some cases it might
* be NULL, in which case the user should (can) use RedisModule_OpenKey (and CloseKey too).
* when it is provided, it is owned by the caller and will be free when the callback returns.
* - privdata - the user data provided to RedisModule_Scan.
*
* The way it should be used:
* RedisModuleCursor *c = RedisModule_ScanCursorCreate();
* while(RedisModule_Scan(ctx, c, callback, privateData));
* RedisModule_ScanCursorDestroy(c);
*
* It is also possible to use this API from another thread while the lock is acquired durring
* the actuall call to RM_Scan:
* RedisModuleCursor *c = RedisModule_ScanCursorCreate();
* RedisModule_ThreadSafeContextLock(ctx);
* while(RedisModule_Scan(ctx, c, callback, privateData)){
* RedisModule_ThreadSafeContextUnlock(ctx);
* // do some background job
* RedisModule_ThreadSafeContextLock(ctx);
* }
* RedisModule_ScanCursorDestroy(c);
*
* The function will return 1 if there are more elements to scan and 0 otherwise,
* possibly setting errno if the call failed.
* It is also possible to restart and existing cursor using RM_CursorRestart. */
int
RM_Scan
(
RedisModuleCtx
*
ctx
,
RedisModuleScanCursor
*
cursor
,
RedisModuleScanCB
fn
,
void
*
privdata
)
{
if
(
cursor
->
done
)
{
errno
=
ENOENT
;
return
0
;
}
int
ret
=
1
;
ScanCBData
data
=
{
ctx
,
privdata
,
fn
};
cursor
->
cursor
=
dictScan
(
ctx
->
client
->
db
->
dict
,
cursor
->
cursor
,
moduleScanCallback
,
NULL
,
&
data
);
if
(
cursor
->
cursor
==
0
)
{
cursor
->
done
=
1
;
ret
=
0
;
}
errno
=
0
;
return
ret
;
}
typedef
void
(
*
RedisModuleScanKeyCB
)(
RedisModuleKey
*
key
,
RedisModuleString
*
field
,
RedisModuleString
*
value
,
void
*
privdata
);
typedef
struct
{
RedisModuleKey
*
key
;
void
*
user_data
;
RedisModuleScanKeyCB
fn
;
}
ScanKeyCBData
;
static
void
moduleScanKeyCallback
(
void
*
privdata
,
const
dictEntry
*
de
)
{
ScanKeyCBData
*
data
=
privdata
;
sds
key
=
dictGetKey
(
de
);
robj
*
o
=
data
->
key
->
value
;
robj
*
field
=
createStringObject
(
key
,
sdslen
(
key
));
robj
*
value
=
NULL
;
if
(
o
->
type
==
OBJ_SET
)
{
value
=
NULL
;
}
else
if
(
o
->
type
==
OBJ_HASH
)
{
sds
val
=
dictGetVal
(
de
);
value
=
createStringObject
(
val
,
sdslen
(
val
));
}
else
if
(
o
->
type
==
OBJ_ZSET
)
{
double
*
val
=
(
double
*
)
dictGetVal
(
de
);
value
=
createStringObjectFromLongDouble
(
*
val
,
0
);
}
data
->
fn
(
data
->
key
,
field
,
value
,
data
->
user_data
);
decrRefCount
(
field
);
if
(
value
)
decrRefCount
(
value
);
}
/* Scan api that allows a module to scan the elements in a hash, set or sorted set key
*
* Callback for scan implementation.
* void scan_callback(RedisModuleKey *key, RedisModuleString* field, RedisModuleString* value, void *privdata);
* - key - the redis key context provided to for the scan.
* - field - field name, owned by the caller and need to be retained if used
* after this function.
* - value - value string or NULL for set type, owned by the caller and need to
* be retained if used after this function.
* - privdata - the user data provided to RedisModule_ScanKey.
*
* The way it should be used:
* RedisModuleCursor *c = RedisModule_ScanCursorCreate();
* RedisModuleKey *key = RedisModule_OpenKey(...)
* while(RedisModule_ScanKey(key, c, callback, privateData));
* RedisModule_CloseKey(key);
* RedisModule_ScanCursorDestroy(c);
*
* It is also possible to use this API from another thread while the lock is acquired durring
* the actuall call to RM_Scan, and re-opening the key each time:
* RedisModuleCursor *c = RedisModule_ScanCursorCreate();
* RedisModule_ThreadSafeContextLock(ctx);
* RedisModuleKey *key = RedisModule_OpenKey(...)
* while(RedisModule_ScanKey(ctx, c, callback, privateData)){
* RedisModule_CloseKey(key);
* RedisModule_ThreadSafeContextUnlock(ctx);
* // do some background job
* RedisModule_ThreadSafeContextLock(ctx);
* RedisModuleKey *key = RedisModule_OpenKey(...)
* }
* RedisModule_CloseKey(key);
* RedisModule_ScanCursorDestroy(c);
*
* The function will return 1 if there are more elements to scan and 0 otherwise,
* possibly setting errno if the call failed.
* It is also possible to restart and existing cursor using RM_CursorRestart. */
int
RM_ScanKey
(
RedisModuleKey
*
key
,
RedisModuleScanCursor
*
cursor
,
RedisModuleScanKeyCB
fn
,
void
*
privdata
)
{
if
(
key
==
NULL
||
key
->
value
==
NULL
)
{
errno
=
EINVAL
;
return
0
;
}
dict
*
ht
=
NULL
;
robj
*
o
=
key
->
value
;
if
(
o
->
type
==
OBJ_SET
)
{
if
(
o
->
encoding
==
OBJ_ENCODING_HT
)
ht
=
o
->
ptr
;
}
else
if
(
o
->
type
==
OBJ_HASH
)
{
if
(
o
->
encoding
==
OBJ_ENCODING_HT
)
ht
=
o
->
ptr
;
}
else
if
(
o
->
type
==
OBJ_ZSET
)
{
if
(
o
->
encoding
==
OBJ_ENCODING_SKIPLIST
)
ht
=
((
zset
*
)
o
->
ptr
)
->
dict
;
}
else
{
errno
=
EINVAL
;
return
0
;
}
if
(
cursor
->
done
)
{
errno
=
ENOENT
;
return
0
;
}
int
ret
=
1
;
if
(
ht
)
{
ScanKeyCBData
data
=
{
key
,
privdata
,
fn
};
cursor
->
cursor
=
dictScan
(
ht
,
cursor
->
cursor
,
moduleScanKeyCallback
,
NULL
,
&
data
);
if
(
cursor
->
cursor
==
0
)
{
cursor
->
done
=
1
;
ret
=
0
;
}
}
else
if
(
o
->
type
==
OBJ_SET
&&
o
->
encoding
==
OBJ_ENCODING_INTSET
)
{
int
pos
=
0
;
int64_t
ll
;
while
(
intsetGet
(
o
->
ptr
,
pos
++
,
&
ll
))
{
robj
*
field
=
createStringObjectFromLongLong
(
ll
);
fn
(
key
,
field
,
NULL
,
privdata
);
decrRefCount
(
field
);
}
cursor
->
cursor
=
1
;
cursor
->
done
=
1
;
ret
=
0
;
}
else
if
(
o
->
type
==
OBJ_HASH
||
o
->
type
==
OBJ_ZSET
)
{
unsigned
char
*
p
=
ziplistIndex
(
o
->
ptr
,
0
);
unsigned
char
*
vstr
;
unsigned
int
vlen
;
long
long
vll
;
while
(
p
)
{
ziplistGet
(
p
,
&
vstr
,
&
vlen
,
&
vll
);
robj
*
field
=
(
vstr
!=
NULL
)
?
createStringObject
((
char
*
)
vstr
,
vlen
)
:
createStringObjectFromLongLong
(
vll
);
p
=
ziplistNext
(
o
->
ptr
,
p
);
ziplistGet
(
p
,
&
vstr
,
&
vlen
,
&
vll
);
robj
*
value
=
(
vstr
!=
NULL
)
?
createStringObject
((
char
*
)
vstr
,
vlen
)
:
createStringObjectFromLongLong
(
vll
);
fn
(
key
,
field
,
value
,
privdata
);
p
=
ziplistNext
(
o
->
ptr
,
p
);
decrRefCount
(
field
);
decrRefCount
(
value
);
}
cursor
->
cursor
=
1
;
cursor
->
done
=
1
;
ret
=
0
;
}
errno
=
0
;
return
ret
;
}
/* --------------------------------------------------------------------------
* Module fork API
* -------------------------------------------------------------------------- */
/* Create a background child process with the current frozen snaphost of the
* main process where you can do some processing in the background without
* affecting / freezing the traffic and no need for threads and GIL locking.
* Note that Redis allows for only one concurrent fork.
* When the child wants to exit, it should call RedisModule_ExitFromChild.
* If the parent wants to kill the child it should call RedisModule_KillForkChild
* The done handler callback will be executed on the parent process when the
* child existed (but not when killed)
* Return: -1 on failure, on success the parent process will get a positive PID
* of the child, and the child process will get 0.
*/
int
RM_Fork
(
RedisModuleForkDoneHandler
cb
,
void
*
user_data
)
{
pid_t
childpid
;
if
(
hasActiveChildProcess
())
{
return
-
1
;
}
openChildInfoPipe
();
if
((
childpid
=
redisFork
())
==
0
)
{
/* Child */
redisSetProcTitle
(
"redis-module-fork"
);
}
else
if
(
childpid
==
-
1
)
{
closeChildInfoPipe
();
serverLog
(
LL_WARNING
,
"Can't fork for module: %s"
,
strerror
(
errno
));
}
else
{
/* Parent */
server
.
module_child_pid
=
childpid
;
moduleForkInfo
.
done_handler
=
cb
;
moduleForkInfo
.
done_handler_user_data
=
user_data
;
serverLog
(
LL_NOTICE
,
"Module fork started pid: %d "
,
childpid
);
}
return
childpid
;
}
/* Call from the child process when you want to terminate it.
* retcode will be provided to the done handler executed on the parent process.
*/
int
RM_ExitFromChild
(
int
retcode
)
{
sendChildCOWInfo
(
CHILD_INFO_TYPE_MODULE
,
"Module fork"
);
exitFromChild
(
retcode
);
return
REDISMODULE_OK
;
}
/* Kill the active module forked child, if there is one active and the
* pid matches, and returns C_OK. Otherwise if there is no active module
* child or the pid does not match, return C_ERR without doing anything. */
int
TerminateModuleForkChild
(
int
child_pid
,
int
wait
)
{
/* Module child should be active and pid should match. */
if
(
server
.
module_child_pid
==
-
1
||
server
.
module_child_pid
!=
child_pid
)
return
C_ERR
;
int
statloc
;
serverLog
(
LL_NOTICE
,
"Killing running module fork child: %ld"
,
(
long
)
server
.
module_child_pid
);
if
(
kill
(
server
.
module_child_pid
,
SIGUSR1
)
!=
-
1
&&
wait
)
{
while
(
wait4
(
server
.
module_child_pid
,
&
statloc
,
0
,
NULL
)
!=
server
.
module_child_pid
);
}
/* Reset the buffer accumulating changes while the child saves. */
server
.
module_child_pid
=
-
1
;
moduleForkInfo
.
done_handler
=
NULL
;
moduleForkInfo
.
done_handler_user_data
=
NULL
;
closeChildInfoPipe
();
updateDictResizePolicy
();
return
C_OK
;
}
/* Can be used to kill the forked child process from the parent process.
* child_pid whould be the return value of RedisModule_Fork. */
int
RM_KillForkChild
(
int
child_pid
)
{
/* Kill module child, wait for child exit. */
if
(
TerminateModuleForkChild
(
child_pid
,
1
)
==
C_OK
)
return
REDISMODULE_OK
;
else
return
REDISMODULE_ERR
;
}
void
ModuleForkDoneHandler
(
int
exitcode
,
int
bysignal
)
{
serverLog
(
LL_NOTICE
,
"Module fork exited pid: %d, retcode: %d, bysignal: %d"
,
server
.
module_child_pid
,
exitcode
,
bysignal
);
if
(
moduleForkInfo
.
done_handler
)
{
moduleForkInfo
.
done_handler
(
exitcode
,
bysignal
,
moduleForkInfo
.
done_handler_user_data
);
}
server
.
module_child_pid
=
-
1
;
moduleForkInfo
.
done_handler
=
NULL
;
moduleForkInfo
.
done_handler_user_data
=
NULL
;
}
/* --------------------------------------------------------------------------
* Server hooks implementation
* -------------------------------------------------------------------------- */
/* Register to be notified, via a callback, when the specified server event
* happens. The callback is called with the event as argument, and an additional
* argument which is a void pointer and should be cased to a specific type
* that is event-specific (but many events will just use NULL since they do not
* have additional information to pass to the callback).
*
* If the callback is NULL and there was a previous subscription, the module
* will be unsubscribed. If there was a previous subscription and the callback
* is not null, the old callback will be replaced with the new one.
*
* The callback must be of this type:
*
* int (*RedisModuleEventCallback)(RedisModuleCtx *ctx,
* RedisModuleEvent eid,
* uint64_t subevent,
* void *data);
*
* The 'ctx' is a normal Redis module context that the callback can use in
* order to call other modules APIs. The 'eid' is the event itself, this
* is only useful in the case the module subscribed to multiple events: using
* the 'id' field of this structure it is possible to check if the event
* is one of the events we registered with this callback. The 'subevent' field
* depends on the event that fired.
*
* Finally the 'data' pointer may be populated, only for certain events, with
* more relevant data.
*
* Here is a list of events you can use as 'eid' and related sub events:
*
* RedisModuleEvent_ReplicationRoleChanged
*
* This event is called when the instance switches from master
* to replica or the other way around, however the event is
* also called when the replica remains a replica but starts to
* replicate with a different master.
*
* The following sub events are available:
*
* REDISMODULE_SUBEVENT_REPLROLECHANGED_NOW_MASTER
* REDISMODULE_SUBEVENT_REPLROLECHANGED_NOW_REPLICA
*
* The 'data' field can be casted by the callback to a
* RedisModuleReplicationInfo structure with the following fields:
*
* int master; // true if master, false if replica
* char *masterhost; // master instance hostname for NOW_REPLICA
* int masterport; // master instance port for NOW_REPLICA
* char *replid1; // Main replication ID
* char *replid2; // Secondary replication ID
* uint64_t repl1_offset; // Main replication offset
* uint64_t repl2_offset; // Offset of replid2 validity
*
* RedisModuleEvent_Persistence
*
* This event is called when RDB saving or AOF rewriting starts
* and ends. The following sub events are available:
*
* REDISMODULE_SUBEVENT_PERSISTENCE_RDB_START
* REDISMODULE_SUBEVENT_PERSISTENCE_AOF_START
* REDISMODULE_SUBEVENT_PERSISTENCE_SYNC_RDB_START
* REDISMODULE_SUBEVENT_PERSISTENCE_ENDED
* REDISMODULE_SUBEVENT_PERSISTENCE_FAILED
*
* The above events are triggered not just when the user calls the
* relevant commands like BGSAVE, but also when a saving operation
* or AOF rewriting occurs because of internal server triggers.
* The SYNC_RDB_START sub events are happening in the forground due to
* SAVE command, FLUSHALL, or server shutdown, and the other RDB and
* AOF sub events are executed in a background fork child, so any
* action the module takes can only affect the generated AOF or RDB,
* but will not be reflected in the parent process and affect connected
* clients and commands. Also note that the AOF_START sub event may end
* up saving RDB content in case of an AOF with rdb-preamble.
*
* RedisModuleEvent_FlushDB
*
* The FLUSHALL, FLUSHDB or an internal flush (for instance
* because of replication, after the replica synchronization)
* happened. The following sub events are available:
*
* REDISMODULE_SUBEVENT_FLUSHDB_START
* REDISMODULE_SUBEVENT_FLUSHDB_END
*
* The data pointer can be casted to a RedisModuleFlushInfo
* structure with the following fields:
*
* int32_t async; // True if the flush is done in a thread.
* See for instance FLUSHALL ASYNC.
* In this case the END callback is invoked
* immediately after the database is put
* in the free list of the thread.
* int32_t dbnum; // Flushed database number, -1 for all the DBs
* in the case of the FLUSHALL operation.
*
* The start event is called *before* the operation is initated, thus
* allowing the callback to call DBSIZE or other operation on the
* yet-to-free keyspace.
*
* RedisModuleEvent_Loading
*
* Called on loading operations: at startup when the server is
* started, but also after a first synchronization when the
* replica is loading the RDB file from the master.
* The following sub events are available:
*
* REDISMODULE_SUBEVENT_LOADING_RDB_START
* REDISMODULE_SUBEVENT_LOADING_AOF_START
* REDISMODULE_SUBEVENT_LOADING_REPL_START
* REDISMODULE_SUBEVENT_LOADING_ENDED
* REDISMODULE_SUBEVENT_LOADING_FAILED
*
* Note that AOF loading may start with an RDB data in case of
* rdb-preamble, in which case you'll only recieve an AOF_START event.
*
*
* RedisModuleEvent_ClientChange
*
* Called when a client connects or disconnects.
* The data pointer can be casted to a RedisModuleClientInfo
* structure, documented in RedisModule_GetClientInfoById().
* The following sub events are available:
*
* REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED
* REDISMODULE_SUBEVENT_CLIENT_CHANGE_DISCONNECTED
*
* RedisModuleEvent_Shutdown
*
* The server is shutting down. No subevents are available.
*
* RedisModuleEvent_ReplicaChange
*
* This event is called when the instance (that can be both a
* master or a replica) get a new online replica, or lose a
* replica since it gets disconnected.
* The following sub events are availble:
*
* REDISMODULE_SUBEVENT_REPLICA_CHANGE_ONLINE
* REDISMODULE_SUBEVENT_REPLICA_CHANGE_OFFLINE
*
* No additional information is available so far: future versions
* of Redis will have an API in order to enumerate the replicas
* connected and their state.
*
* RedisModuleEvent_CronLoop
*
* This event is called every time Redis calls the serverCron()
* function in order to do certain bookkeeping. Modules that are
* required to do operations from time to time may use this callback.
* Normally Redis calls this function 10 times per second, but
* this changes depending on the "hz" configuration.
* No sub events are available.
*
* The data pointer can be casted to a RedisModuleCronLoop
* structure with the following fields:
*
* int32_t hz; // Approximate number of events per second.
*
* RedisModuleEvent_MasterLinkChange
*
* This is called for replicas in order to notify when the
* replication link becomes functional (up) with our master,
* or when it goes down. Note that the link is not considered
* up when we just connected to the master, but only if the
* replication is happening correctly.
* The following sub events are available:
*
* REDISMODULE_SUBEVENT_MASTER_LINK_UP
* REDISMODULE_SUBEVENT_MASTER_LINK_DOWN
*
* RedisModuleEvent_ModuleChange
*
* This event is called when a new module is loaded or one is unloaded.
* The following sub events are availble:
*
* REDISMODULE_SUBEVENT_MODULE_LOADED
* REDISMODULE_SUBEVENT_MODULE_UNLOADED
*
* The data pointer can be casted to a RedisModuleModuleChange
* structure with the following fields:
*
* const char* module_name; // Name of module loaded or unloaded.
* int32_t module_version; // Module version.
*
* RedisModuleEvent_LoadingProgress
*
* This event is called repeatedly called while an RDB or AOF file
* is being loaded.
* The following sub events are availble:
*
* REDISMODULE_SUBEVENT_LOADING_PROGRESS_RDB
* REDISMODULE_SUBEVENT_LOADING_PROGRESS_AOF
*
* The data pointer can be casted to a RedisModuleLoadingProgress
* structure with the following fields:
*
* int32_t hz; // Approximate number of events per second.
* int32_t progress; // Approximate progress between 0 and 1024,
* or -1 if unknown.
*
* The function returns REDISMODULE_OK if the module was successfully subscrived
* for the specified event. If the API is called from a wrong context then
* REDISMODULE_ERR is returned. */
int
RM_SubscribeToServerEvent
(
RedisModuleCtx
*
ctx
,
RedisModuleEvent
event
,
RedisModuleEventCallback
callback
)
{
RedisModuleEventListener
*
el
;
/* Protect in case of calls from contexts without a module reference. */
if
(
ctx
->
module
==
NULL
)
return
REDISMODULE_ERR
;
/* Search an event matching this module and event ID. */
listIter
li
;
listNode
*
ln
;
listRewind
(
RedisModule_EventListeners
,
&
li
);
while
((
ln
=
listNext
(
&
li
)))
{
el
=
ln
->
value
;
if
(
el
->
module
==
ctx
->
module
&&
el
->
event
.
id
==
event
.
id
)
break
;
/* Matching event found. */
}
/* Modify or remove the event listener if we already had one. */
if
(
ln
)
{
if
(
callback
==
NULL
)
{
listDelNode
(
RedisModule_EventListeners
,
ln
);
zfree
(
el
);
}
else
{
el
->
callback
=
callback
;
/* Update the callback with the new one. */
}
return
REDISMODULE_OK
;
}
/* No event found, we need to add a new one. */
el
=
zmalloc
(
sizeof
(
*
el
));
el
->
module
=
ctx
->
module
;
el
->
event
=
event
;
el
->
callback
=
callback
;
listAddNodeTail
(
RedisModule_EventListeners
,
el
);
return
REDISMODULE_OK
;
}
/* This is called by the Redis internals every time we want to fire an
* event that can be interceppted by some module. The pointer 'data' is useful
* in order to populate the event-specific structure when needed, in order
* to return the structure with more information to the callback.
*
* 'eid' and 'subid' are just the main event ID and the sub event associated
* with the event, depending on what exactly happened. */
void
moduleFireServerEvent
(
uint64_t
eid
,
int
subid
,
void
*
data
)
{
/* Fast path to return ASAP if there is nothing to do, avoiding to
* setup the iterator and so forth: we want this call to be extremely
* cheap if there are no registered modules. */
if
(
listLength
(
RedisModule_EventListeners
)
==
0
)
return
;
listIter
li
;
listNode
*
ln
;
listRewind
(
RedisModule_EventListeners
,
&
li
);
while
((
ln
=
listNext
(
&
li
)))
{
RedisModuleEventListener
*
el
=
ln
->
value
;
if
(
el
->
event
.
id
==
eid
)
{
RedisModuleCtx
ctx
=
REDISMODULE_CTX_INIT
;
ctx
.
module
=
el
->
module
;
if
(
ModulesInHooks
==
0
)
{
ctx
.
client
=
moduleFreeContextReusedClient
;
}
else
{
ctx
.
client
=
createClient
(
NULL
);
ctx
.
client
->
flags
|=
CLIENT_MODULE
;
ctx
.
client
->
user
=
NULL
;
/* Root user. */
}
void
*
moduledata
=
NULL
;
RedisModuleClientInfoV1
civ1
;
RedisModuleReplicationInfoV1
riv1
;
RedisModuleModuleChangeV1
mcv1
;
/* Start at DB zero by default when calling the handler. It's
* up to the specific event setup to change it when it makes
* sense. For instance for FLUSHDB events we select the correct
* DB automatically. */
selectDb
(
ctx
.
client
,
0
);
/* Event specific context and data pointer setup. */
if
(
eid
==
REDISMODULE_EVENT_CLIENT_CHANGE
)
{
modulePopulateClientInfoStructure
(
&
civ1
,
data
,
el
->
event
.
dataver
);
moduledata
=
&
civ1
;
}
else
if
(
eid
==
REDISMODULE_EVENT_REPLICATION_ROLE_CHANGED
)
{
modulePopulateReplicationInfoStructure
(
&
riv1
,
el
->
event
.
dataver
);
moduledata
=
&
riv1
;
}
else
if
(
eid
==
REDISMODULE_EVENT_FLUSHDB
)
{
moduledata
=
data
;
RedisModuleFlushInfoV1
*
fi
=
data
;
if
(
fi
->
dbnum
!=
-
1
)
selectDb
(
ctx
.
client
,
fi
->
dbnum
);
}
else
if
(
eid
==
REDISMODULE_EVENT_MODULE_CHANGE
)
{
RedisModule
*
m
=
data
;
if
(
m
==
el
->
module
)
continue
;
mcv1
.
version
=
REDISMODULE_MODULE_CHANGE_VERSION
;
mcv1
.
module_name
=
m
->
name
;
mcv1
.
module_version
=
m
->
ver
;
moduledata
=
&
mcv1
;
}
else
if
(
eid
==
REDISMODULE_EVENT_LOADING_PROGRESS
)
{
moduledata
=
data
;
}
else
if
(
eid
==
REDISMODULE_EVENT_CRON_LOOP
)
{
moduledata
=
data
;
}
ModulesInHooks
++
;
el
->
module
->
in_hook
++
;
el
->
callback
(
&
ctx
,
el
->
event
,
subid
,
moduledata
);
el
->
module
->
in_hook
--
;
ModulesInHooks
--
;
if
(
ModulesInHooks
!=
0
)
freeClient
(
ctx
.
client
);
moduleFreeContext
(
&
ctx
);
}
}
}
/* Remove all the listeners for this module: this is used before unloading
* a module. */
void
moduleUnsubscribeAllServerEvents
(
RedisModule
*
module
)
{
RedisModuleEventListener
*
el
;
listIter
li
;
listNode
*
ln
;
listRewind
(
RedisModule_EventListeners
,
&
li
);
while
((
ln
=
listNext
(
&
li
)))
{
el
=
ln
->
value
;
if
(
el
->
module
==
module
)
{
listDelNode
(
RedisModule_EventListeners
,
ln
);
zfree
(
el
);
}
}
}
void
processModuleLoadingProgressEvent
(
int
is_aof
)
{
long
long
now
=
ustime
();
static
long
long
next_event
=
0
;
if
(
now
>=
next_event
)
{
/* Fire the loading progress modules end event. */
int
progress
=
-
1
;
if
(
server
.
loading_total_bytes
)
progress
=
(
server
.
loading_total_bytes
<<
10
)
/
server
.
loading_total_bytes
;
RedisModuleFlushInfoV1
fi
=
{
REDISMODULE_LOADING_PROGRESS_VERSION
,
server
.
hz
,
progress
};
moduleFireServerEvent
(
REDISMODULE_EVENT_LOADING_PROGRESS
,
is_aof
?
REDISMODULE_SUBEVENT_LOADING_PROGRESS_AOF:
REDISMODULE_SUBEVENT_LOADING_PROGRESS_RDB
,
&
fi
);
/* decide when the next event should fire. */
next_event
=
now
+
1000000
/
server
.
hz
;
}
}
/* --------------------------------------------------------------------------
* Modules API internals
* -------------------------------------------------------------------------- */
...
...
@@ -5071,7 +6891,7 @@ void moduleInitModulesSystem(void) {
/* Set up the keyspace notification susbscriber list and static client */
moduleKeyspaceSubscribers
=
listCreate
();
moduleFreeContextReusedClient
=
createClient
(
-
1
);
moduleFreeContextReusedClient
=
createClient
(
NULL
);
moduleFreeContextReusedClient
->
flags
|=
CLIENT_MODULE
;
moduleFreeContextReusedClient
->
user
=
NULL
;
/* root user. */
...
...
@@ -5093,6 +6913,9 @@ void moduleInitModulesSystem(void) {
/* Create the timers radix tree. */
Timers
=
raxNew
();
/* Setup the event listeners data structures. */
RedisModule_EventListeners
=
listCreate
();
/* Our thread-safe contexts GIL must start with already locked:
* it is just unlocked when it's safe. */
pthread_mutex_lock
(
&
moduleGIL
);
...
...
@@ -5128,6 +6951,8 @@ void moduleLoadFromQueue(void) {
void
moduleFreeModuleStructure
(
struct
RedisModule
*
module
)
{
listRelease
(
module
->
types
);
listRelease
(
module
->
filters
);
listRelease
(
module
->
usedby
);
listRelease
(
module
->
using
);
sdsfree
(
module
->
name
);
zfree
(
module
);
}
...
...
@@ -5198,8 +7023,14 @@ int moduleLoad(const char *path, void **module_argv, int module_argc) {
/* Redis module loaded! Register it. */
dictAdd
(
modules
,
ctx
.
module
->
name
,
ctx
.
module
);
ctx
.
module
->
blocked_clients
=
0
;
ctx
.
module
->
handle
=
handle
;
serverLog
(
LL_NOTICE
,
"Module '%s' loaded from %s"
,
ctx
.
module
->
name
,
path
);
/* Fire the loaded modules event. */
moduleFireServerEvent
(
REDISMODULE_EVENT_MODULE_CHANGE
,
REDISMODULE_SUBEVENT_MODULE_LOADED
,
ctx
.
module
);
moduleFreeContext
(
&
ctx
);
return
C_OK
;
}
...
...
@@ -5223,6 +7054,26 @@ int moduleUnload(sds name) {
}
else
if
(
listLength
(
module
->
usedby
))
{
errno
=
EPERM
;
return
REDISMODULE_ERR
;
}
else
if
(
module
->
blocked_clients
)
{
errno
=
EAGAIN
;
return
REDISMODULE_ERR
;
}
/* Give module a chance to clean up. */
int
(
*
onunload
)(
void
*
);
onunload
=
(
int
(
*
)(
void
*
))(
unsigned
long
)
dlsym
(
module
->
handle
,
"RedisModule_OnUnload"
);
if
(
onunload
)
{
RedisModuleCtx
ctx
=
REDISMODULE_CTX_INIT
;
ctx
.
module
=
module
;
ctx
.
client
=
moduleFreeContextReusedClient
;
int
unload_status
=
onunload
((
void
*
)
&
ctx
);
moduleFreeContext
(
&
ctx
);
if
(
unload_status
==
REDISMODULE_ERR
)
{
serverLog
(
LL_WARNING
,
"Module %s OnUnload failed. Unload canceled."
,
name
);
errno
=
ECANCELED
;
return
REDISMODULE_ERR
;
}
}
moduleUnregisterCommands
(
module
);
...
...
@@ -5232,8 +7083,7 @@ int moduleUnload(sds name) {
/* Remove any notification subscribers this module might have */
moduleUnsubscribeNotifications
(
module
);
/* Unregister all the hooks. TODO: Yet no hooks support here. */
moduleUnsubscribeAllServerEvents
(
module
);
/* Unload the dynamic library. */
if
(
dlclose
(
module
->
handle
)
==
-
1
)
{
...
...
@@ -5243,6 +7093,11 @@ int moduleUnload(sds name) {
module
->
name
,
error
);
}
/* Fire the unloaded modules event. */
moduleFireServerEvent
(
REDISMODULE_EVENT_MODULE_CHANGE
,
REDISMODULE_SUBEVENT_MODULE_UNLOADED
,
module
);
/* Remove from list of modules. */
serverLog
(
LL_NOTICE
,
"Module %s unloaded"
,
module
->
name
);
dictDelete
(
modules
,
module
->
name
);
...
...
@@ -5271,6 +7126,62 @@ void addReplyLoadedModules(client *c) {
dictReleaseIterator
(
di
);
}
/* Helper for genModulesInfoString(): given a list of modules, return
* am SDS string in the form "[modulename|modulename2|...]" */
sds
genModulesInfoStringRenderModulesList
(
list
*
l
)
{
listIter
li
;
listNode
*
ln
;
listRewind
(
l
,
&
li
);
sds
output
=
sdsnew
(
"["
);
while
((
ln
=
listNext
(
&
li
)))
{
RedisModule
*
module
=
ln
->
value
;
output
=
sdscat
(
output
,
module
->
name
);
}
output
=
sdstrim
(
output
,
"|"
);
output
=
sdscat
(
output
,
"]"
);
return
output
;
}
/* Helper for genModulesInfoString(): render module options as an SDS string. */
sds
genModulesInfoStringRenderModuleOptions
(
struct
RedisModule
*
module
)
{
sds
output
=
sdsnew
(
"["
);
if
(
module
->
options
&
REDISMODULE_OPTIONS_HANDLE_IO_ERRORS
)
output
=
sdscat
(
output
,
"handle-io-errors|"
);
output
=
sdstrim
(
output
,
"|"
);
output
=
sdscat
(
output
,
"]"
);
return
output
;
}
/* Helper function for the INFO command: adds loaded modules as to info's
* output.
*
* After the call, the passed sds info string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds
genModulesInfoString
(
sds
info
)
{
dictIterator
*
di
=
dictGetIterator
(
modules
);
dictEntry
*
de
;
while
((
de
=
dictNext
(
di
))
!=
NULL
)
{
sds
name
=
dictGetKey
(
de
);
struct
RedisModule
*
module
=
dictGetVal
(
de
);
sds
usedby
=
genModulesInfoStringRenderModulesList
(
module
->
usedby
);
sds
using
=
genModulesInfoStringRenderModulesList
(
module
->
using
);
sds
options
=
genModulesInfoStringRenderModuleOptions
(
module
);
info
=
sdscatfmt
(
info
,
"module:name=%S,ver=%i,api=%i,filters=%i,"
"usedby=%S,using=%S,options=%S
\r\n
"
,
name
,
module
->
ver
,
module
->
apiver
,
(
int
)
listLength
(
module
->
filters
),
usedby
,
using
,
options
);
sdsfree
(
usedby
);
sdsfree
(
using
);
sdsfree
(
options
);
}
dictReleaseIterator
(
di
);
return
info
;
}
/* Redis MODULE command.
*
* MODULE LOAD <path> [args...] */
...
...
@@ -5316,6 +7227,10 @@ NULL
errmsg
=
"the module exports APIs used by other modules. "
"Please unload them first and try again"
;
break
;
case
EAGAIN
:
errmsg
=
"the module has blocked clients. "
"Please wait them unblocked and try again"
;
break
;
default:
errmsg
=
"operation not possible."
;
break
;
...
...
@@ -5335,6 +7250,82 @@ size_t moduleCount(void) {
return
dictSize
(
modules
);
}
/* Set the key last access time for LRU based eviction. not relevent if the
* servers's maxmemory policy is LFU based. Value is idle time in milliseconds.
* returns REDISMODULE_OK if the LRU was updated, REDISMODULE_ERR otherwise. */
int
RM_SetLRU
(
RedisModuleKey
*
key
,
mstime_t
lru_idle
)
{
if
(
!
key
->
value
)
return
REDISMODULE_ERR
;
if
(
objectSetLRUOrLFU
(
key
->
value
,
-
1
,
lru_idle
,
lru_idle
>=
0
?
LRU_CLOCK
()
:
0
,
1
))
return
REDISMODULE_OK
;
return
REDISMODULE_ERR
;
}
/* Gets the key last access time.
* Value is idletime in milliseconds or -1 if the server's eviction policy is
* LFU based.
* returns REDISMODULE_OK if when key is valid. */
int
RM_GetLRU
(
RedisModuleKey
*
key
,
mstime_t
*
lru_idle
)
{
*
lru_idle
=
-
1
;
if
(
!
key
->
value
)
return
REDISMODULE_ERR
;
if
(
server
.
maxmemory_policy
&
MAXMEMORY_FLAG_LFU
)
return
REDISMODULE_OK
;
*
lru_idle
=
estimateObjectIdleTime
(
key
->
value
);
return
REDISMODULE_OK
;
}
/* Set the key access frequency. only relevant if the server's maxmemory policy
* is LFU based.
* The frequency is a logarithmic counter that provides an indication of
* the access frequencyonly (must be <= 255).
* returns REDISMODULE_OK if the LFU was updated, REDISMODULE_ERR otherwise. */
int
RM_SetLFU
(
RedisModuleKey
*
key
,
long
long
lfu_freq
)
{
if
(
!
key
->
value
)
return
REDISMODULE_ERR
;
if
(
objectSetLRUOrLFU
(
key
->
value
,
lfu_freq
,
-
1
,
0
,
1
))
return
REDISMODULE_OK
;
return
REDISMODULE_ERR
;
}
/* Gets the key access frequency or -1 if the server's eviction policy is not
* LFU based.
* returns REDISMODULE_OK if when key is valid. */
int
RM_GetLFU
(
RedisModuleKey
*
key
,
long
long
*
lfu_freq
)
{
*
lfu_freq
=
-
1
;
if
(
!
key
->
value
)
return
REDISMODULE_ERR
;
if
(
server
.
maxmemory_policy
&
MAXMEMORY_FLAG_LFU
)
*
lfu_freq
=
LFUDecrAndReturn
(
key
->
value
);
return
REDISMODULE_OK
;
}
/* Replace the value assigned to a module type.
*
* The key must be open for writing, have an existing value, and have a moduleType
* that matches the one specified by the caller.
*
* Unlike RM_ModuleTypeSetValue() which will free the old value, this function
* simply swaps the old value with the new value.
*
* The function returns the old value, or NULL if any of the above conditions is
* not met.
*/
void
*
RM_ModuleTypeReplaceValue
(
RedisModuleKey
*
key
,
moduleType
*
mt
,
void
*
new_value
)
{
if
(
!
(
key
->
mode
&
REDISMODULE_WRITE
)
||
key
->
iter
)
return
NULL
;
if
(
!
key
->
value
||
key
->
value
->
type
!=
OBJ_MODULE
)
return
NULL
;
moduleValue
*
mv
=
key
->
value
->
ptr
;
if
(
mv
->
type
!=
mt
)
return
NULL
;
void
*
old_val
=
mv
->
value
;
mv
->
value
=
new_value
;
return
old_val
;
}
/* Register all the APIs we export. Keep this function at the end of the
* file so that's easy to seek it to add new entries. */
void
moduleRegisterCoreAPI
(
void
)
{
...
...
@@ -5353,12 +7344,18 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
ReplyWithError
);
REGISTER_API
(
ReplyWithSimpleString
);
REGISTER_API
(
ReplyWithArray
);
REGISTER_API
(
ReplyWithNullArray
);
REGISTER_API
(
ReplyWithEmptyArray
);
REGISTER_API
(
ReplySetArrayLength
);
REGISTER_API
(
ReplyWithString
);
REGISTER_API
(
ReplyWithEmptyString
);
REGISTER_API
(
ReplyWithVerbatimString
);
REGISTER_API
(
ReplyWithStringBuffer
);
REGISTER_API
(
ReplyWithCString
);
REGISTER_API
(
ReplyWithNull
);
REGISTER_API
(
ReplyWithCallReply
);
REGISTER_API
(
ReplyWithDouble
);
REGISTER_API
(
ReplyWithLongDouble
);
REGISTER_API
(
GetSelectedDb
);
REGISTER_API
(
SelectDb
);
REGISTER_API
(
OpenKey
);
...
...
@@ -5369,6 +7366,7 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
ListPop
);
REGISTER_API
(
StringToLongLong
);
REGISTER_API
(
StringToDouble
);
REGISTER_API
(
StringToLongDouble
);
REGISTER_API
(
Call
);
REGISTER_API
(
CallReplyProto
);
REGISTER_API
(
FreeCallReply
);
...
...
@@ -5380,6 +7378,7 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
CreateStringFromCallReply
);
REGISTER_API
(
CreateString
);
REGISTER_API
(
CreateStringFromLongLong
);
REGISTER_API
(
CreateStringFromLongDouble
);
REGISTER_API
(
CreateStringFromString
);
REGISTER_API
(
CreateStringPrintf
);
REGISTER_API
(
FreeString
);
...
...
@@ -5394,6 +7393,9 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
StringTruncate
);
REGISTER_API
(
SetExpire
);
REGISTER_API
(
GetExpire
);
REGISTER_API
(
ResetDataset
);
REGISTER_API
(
DbSize
);
REGISTER_API
(
RandomKey
);
REGISTER_API
(
ZsetAdd
);
REGISTER_API
(
ZsetIncrby
);
REGISTER_API
(
ZsetScore
);
...
...
@@ -5416,8 +7418,12 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
PoolAlloc
);
REGISTER_API
(
CreateDataType
);
REGISTER_API
(
ModuleTypeSetValue
);
REGISTER_API
(
ModuleTypeReplaceValue
);
REGISTER_API
(
ModuleTypeGetType
);
REGISTER_API
(
ModuleTypeGetValue
);
REGISTER_API
(
IsIOError
);
REGISTER_API
(
SetModuleOptions
);
REGISTER_API
(
SignalModifiedKey
);
REGISTER_API
(
SaveUnsigned
);
REGISTER_API
(
LoadUnsigned
);
REGISTER_API
(
SaveSigned
);
...
...
@@ -5430,14 +7436,21 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
LoadDouble
);
REGISTER_API
(
SaveFloat
);
REGISTER_API
(
LoadFloat
);
REGISTER_API
(
SaveLongDouble
);
REGISTER_API
(
LoadLongDouble
);
REGISTER_API
(
SaveDataTypeToString
);
REGISTER_API
(
LoadDataTypeFromString
);
REGISTER_API
(
EmitAOF
);
REGISTER_API
(
Log
);
REGISTER_API
(
LogIOError
);
REGISTER_API
(
_Assert
);
REGISTER_API
(
LatencyAddSample
);
REGISTER_API
(
StringAppendBuffer
);
REGISTER_API
(
RetainString
);
REGISTER_API
(
StringCompare
);
REGISTER_API
(
GetContextFromIO
);
REGISTER_API
(
GetKeyNameFromIO
);
REGISTER_API
(
GetKeyNameFromModuleKey
);
REGISTER_API
(
BlockClient
);
REGISTER_API
(
UnblockClient
);
REGISTER_API
(
IsBlockedReplyRequest
);
...
...
@@ -5452,6 +7465,8 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
DigestAddStringBuffer
);
REGISTER_API
(
DigestAddLongLong
);
REGISTER_API
(
DigestEndSequence
);
REGISTER_API
(
NotifyKeyspaceEvent
);
REGISTER_API
(
GetNotifyKeyspaceEvents
);
REGISTER_API
(
SubscribeToKeyspaceEvents
);
REGISTER_API
(
RegisterClusterMessageReceiver
);
REGISTER_API
(
SendClusterMessage
);
...
...
@@ -5500,4 +7515,40 @@ void moduleRegisterCoreAPI(void) {
REGISTER_API
(
CommandFilterArgInsert
);
REGISTER_API
(
CommandFilterArgReplace
);
REGISTER_API
(
CommandFilterArgDelete
);
REGISTER_API
(
Fork
);
REGISTER_API
(
ExitFromChild
);
REGISTER_API
(
KillForkChild
);
REGISTER_API
(
RegisterInfoFunc
);
REGISTER_API
(
InfoAddSection
);
REGISTER_API
(
InfoBeginDictField
);
REGISTER_API
(
InfoEndDictField
);
REGISTER_API
(
InfoAddFieldString
);
REGISTER_API
(
InfoAddFieldCString
);
REGISTER_API
(
InfoAddFieldDouble
);
REGISTER_API
(
InfoAddFieldLongLong
);
REGISTER_API
(
InfoAddFieldULongLong
);
REGISTER_API
(
GetServerInfo
);
REGISTER_API
(
FreeServerInfo
);
REGISTER_API
(
ServerInfoGetField
);
REGISTER_API
(
ServerInfoGetFieldC
);
REGISTER_API
(
ServerInfoGetFieldSigned
);
REGISTER_API
(
ServerInfoGetFieldUnsigned
);
REGISTER_API
(
ServerInfoGetFieldDouble
);
REGISTER_API
(
GetClientInfoById
);
REGISTER_API
(
PublishMessage
);
REGISTER_API
(
SubscribeToServerEvent
);
REGISTER_API
(
SetLRU
);
REGISTER_API
(
GetLRU
);
REGISTER_API
(
SetLFU
);
REGISTER_API
(
GetLFU
);
REGISTER_API
(
BlockClientOnKeys
);
REGISTER_API
(
SignalKeyAsReady
);
REGISTER_API
(
GetBlockedClientReadyKey
);
REGISTER_API
(
GetUsedMemoryRatio
);
REGISTER_API
(
MallocSize
);
REGISTER_API
(
ScanCursorCreate
);
REGISTER_API
(
ScanCursorDestroy
);
REGISTER_API
(
ScanCursorRestart
);
REGISTER_API
(
Scan
);
REGISTER_API
(
ScanKey
);
}
src/modules/Makefile
View file @
2968d8e3
...
...
@@ -13,7 +13,7 @@ endif
.SUFFIXES
:
.c .so .xo .o
all
:
helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so
all
:
helloworld.so hellotype.so helloblock.so testmodule.so hellocluster.so hellotimer.so hellodict.so
hellohook.so
.c.xo
:
$(CC)
-I
.
$(CFLAGS)
$(SHOBJ_CFLAGS)
-fPIC
-c
$<
-o
$@
...
...
@@ -46,6 +46,12 @@ hellotimer.so: hellotimer.xo
hellodict.xo
:
../redismodule.h
hellodict.so
:
hellodict.xo
$(LD)
-o
$@
$<
$(SHOBJ_LDFLAGS)
$(LIBS)
-lc
hellohook.xo
:
../redismodule.h
hellohook.so
:
hellohook.xo
$(LD)
-o
$@
$<
$(SHOBJ_LDFLAGS)
$(LIBS)
-lc
testmodule.xo
:
../redismodule.h
...
...
src/modules/hellohook.c
0 → 100644
View file @
2968d8e3
/* Server hooks API example
*
* -----------------------------------------------------------------------------
*
* Copyright (c) 2019, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Redis nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#define REDISMODULE_EXPERIMENTAL_API
#include "../redismodule.h"
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
/* Client state change callback. */
void
clientChangeCallback
(
RedisModuleCtx
*
ctx
,
RedisModuleEvent
e
,
uint64_t
sub
,
void
*
data
)
{
REDISMODULE_NOT_USED
(
ctx
);
REDISMODULE_NOT_USED
(
e
);
RedisModuleClientInfo
*
ci
=
data
;
printf
(
"Client %s event for client #%llu %s:%d
\n
"
,
(
sub
==
REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED
)
?
"connection"
:
"disconnection"
,
ci
->
id
,
ci
->
addr
,
ci
->
port
);
}
void
flushdbCallback
(
RedisModuleCtx
*
ctx
,
RedisModuleEvent
e
,
uint64_t
sub
,
void
*
data
)
{
REDISMODULE_NOT_USED
(
ctx
);
REDISMODULE_NOT_USED
(
e
);
RedisModuleFlushInfo
*
fi
=
data
;
if
(
sub
==
REDISMODULE_SUBEVENT_FLUSHDB_START
)
{
if
(
fi
->
dbnum
!=
-
1
)
{
RedisModuleCallReply
*
reply
;
reply
=
RedisModule_Call
(
ctx
,
"DBSIZE"
,
""
);
long
long
numkeys
=
RedisModule_CallReplyInteger
(
reply
);
printf
(
"FLUSHDB event of database %d started (%lld keys in DB)
\n
"
,
fi
->
dbnum
,
numkeys
);
RedisModule_FreeCallReply
(
reply
);
}
else
{
printf
(
"FLUSHALL event started
\n
"
);
}
}
else
{
if
(
fi
->
dbnum
!=
-
1
)
{
printf
(
"FLUSHDB event of database %d ended
\n
"
,
fi
->
dbnum
);
}
else
{
printf
(
"FLUSHALL event ended
\n
"
);
}
}
}
/* This function must be present on each Redis module. It is used in order to
* register the commands into the Redis server. */
int
RedisModule_OnLoad
(
RedisModuleCtx
*
ctx
,
RedisModuleString
**
argv
,
int
argc
)
{
REDISMODULE_NOT_USED
(
argv
);
REDISMODULE_NOT_USED
(
argc
);
if
(
RedisModule_Init
(
ctx
,
"hellohook"
,
1
,
REDISMODULE_APIVER_1
)
==
REDISMODULE_ERR
)
return
REDISMODULE_ERR
;
RedisModule_SubscribeToServerEvent
(
ctx
,
RedisModuleEvent_ClientChange
,
clientChangeCallback
);
RedisModule_SubscribeToServerEvent
(
ctx
,
RedisModuleEvent_FlushDB
,
flushdbCallback
);
return
REDISMODULE_OK
;
}
src/modules/hellotype.c
View file @
2968d8e3
...
...
@@ -129,6 +129,7 @@ int HelloTypeInsert_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv,
/* Insert the new element. */
HelloTypeInsert
(
hto
,
value
);
RedisModule_SignalKeyAsReady
(
ctx
,
argv
[
1
]);
RedisModule_ReplyWithLongLong
(
ctx
,
hto
->
len
);
RedisModule_ReplicateVerbatim
(
ctx
);
...
...
@@ -190,6 +191,77 @@ int HelloTypeLen_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int
return
REDISMODULE_OK
;
}
/* ====================== Example of a blocking command ==================== */
/* Reply callback for blocking command HELLOTYPE.BRANGE, this will get
* called when the key we blocked for is ready: we need to check if we
* can really serve the client, and reply OK or ERR accordingly. */
int
HelloBlock_Reply
(
RedisModuleCtx
*
ctx
,
RedisModuleString
**
argv
,
int
argc
)
{
REDISMODULE_NOT_USED
(
argv
);
REDISMODULE_NOT_USED
(
argc
);
RedisModuleString
*
keyname
=
RedisModule_GetBlockedClientReadyKey
(
ctx
);
RedisModuleKey
*
key
=
RedisModule_OpenKey
(
ctx
,
keyname
,
REDISMODULE_READ
);
int
type
=
RedisModule_KeyType
(
key
);
if
(
type
!=
REDISMODULE_KEYTYPE_MODULE
||
RedisModule_ModuleTypeGetType
(
key
)
!=
HelloType
)
{
RedisModule_CloseKey
(
key
);
return
REDISMODULE_ERR
;
}
/* In case the key is able to serve our blocked client, let's directly
* use our original command implementation to make this example simpler. */
RedisModule_CloseKey
(
key
);
return
HelloTypeRange_RedisCommand
(
ctx
,
argv
,
argc
-
1
);
}
/* Timeout callback for blocking command HELLOTYPE.BRANGE */
int
HelloBlock_Timeout
(
RedisModuleCtx
*
ctx
,
RedisModuleString
**
argv
,
int
argc
)
{
REDISMODULE_NOT_USED
(
argv
);
REDISMODULE_NOT_USED
(
argc
);
return
RedisModule_ReplyWithSimpleString
(
ctx
,
"Request timedout"
);
}
/* Private data freeing callback for HELLOTYPE.BRANGE command. */
void
HelloBlock_FreeData
(
RedisModuleCtx
*
ctx
,
void
*
privdata
)
{
REDISMODULE_NOT_USED
(
ctx
);
RedisModule_Free
(
privdata
);
}
/* HELLOTYPE.BRANGE key first count timeout -- This is a blocking verison of
* the RANGE operation, in order to show how to use the API
* RedisModule_BlockClientOnKeys(). */
int
HelloTypeBRange_RedisCommand
(
RedisModuleCtx
*
ctx
,
RedisModuleString
**
argv
,
int
argc
)
{
if
(
argc
!=
5
)
return
RedisModule_WrongArity
(
ctx
);
RedisModule_AutoMemory
(
ctx
);
/* Use automatic memory management. */
RedisModuleKey
*
key
=
RedisModule_OpenKey
(
ctx
,
argv
[
1
],
REDISMODULE_READ
|
REDISMODULE_WRITE
);
int
type
=
RedisModule_KeyType
(
key
);
if
(
type
!=
REDISMODULE_KEYTYPE_EMPTY
&&
RedisModule_ModuleTypeGetType
(
key
)
!=
HelloType
)
{
return
RedisModule_ReplyWithError
(
ctx
,
REDISMODULE_ERRORMSG_WRONGTYPE
);
}
/* Parse the timeout before even trying to serve the client synchronously,
* so that we always fail ASAP on syntax errors. */
long
long
timeout
;
if
(
RedisModule_StringToLongLong
(
argv
[
4
],
&
timeout
)
!=
REDISMODULE_OK
)
{
return
RedisModule_ReplyWithError
(
ctx
,
"ERR invalid timeout parameter"
);
}
/* Can we serve the reply synchronously? */
if
(
type
!=
REDISMODULE_KEYTYPE_EMPTY
)
{
return
HelloTypeRange_RedisCommand
(
ctx
,
argv
,
argc
-
1
);
}
/* Otherwise let's block on the key. */
void
*
privdata
=
RedisModule_Alloc
(
100
);
RedisModule_BlockClientOnKeys
(
ctx
,
HelloBlock_Reply
,
HelloBlock_Timeout
,
HelloBlock_FreeData
,
timeout
,
argv
+
1
,
1
,
privdata
);
return
REDISMODULE_OK
;
}
/* ========================== "hellotype" type methods ======================= */
...
...
@@ -282,5 +354,9 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
HelloTypeLen_RedisCommand
,
"readonly"
,
1
,
1
,
1
)
==
REDISMODULE_ERR
)
return
REDISMODULE_ERR
;
if
(
RedisModule_CreateCommand
(
ctx
,
"hellotype.brange"
,
HelloTypeBRange_RedisCommand
,
"readonly"
,
1
,
1
,
1
)
==
REDISMODULE_ERR
)
return
REDISMODULE_ERR
;
return
REDISMODULE_OK
;
}
src/multi.c
View file @
2968d8e3
...
...
@@ -175,7 +175,19 @@ void execCommand(client *c) {
must_propagate
=
1
;
}
int
acl_retval
=
ACLCheckCommandPerm
(
c
);
if
(
acl_retval
!=
ACL_OK
)
{
addReplyErrorFormat
(
c
,
"-NOPERM ACLs rules changed between the moment the "
"transaction was accumulated and the EXEC call. "
"This command is no longer allowed for the "
"following reason: %s"
,
(
acl_retval
==
ACL_DENIED_CMD
)
?
"no permission to execute the command or subcommand"
:
"no permission to touch the specified keys"
);
}
else
{
call
(
c
,
server
.
loading
?
CMD_CALL_NONE
:
CMD_CALL_FULL
);
}
/* Commands may alter argc/argv, restore mstate. */
c
->
mstate
.
commands
[
j
].
argc
=
c
->
argc
;
...
...
src/networking.c
View file @
2968d8e3
...
...
@@ -84,32 +84,27 @@ void linkClient(client *c) {
raxInsert
(
server
.
clients_index
,(
unsigned
char
*
)
&
id
,
sizeof
(
id
),
c
,
NULL
);
}
client
*
createClient
(
int
fd
)
{
client
*
createClient
(
connection
*
conn
)
{
client
*
c
=
zmalloc
(
sizeof
(
client
));
/* passing
-1 as fd
it is possible to create a non connected client.
/* passing
NULL as conn
it is possible to create a non connected client.
* This is useful since all the commands needs to be executed
* in the context of a client. When commands are executed in other
* contexts (for instance a Lua script) we need a non connected client. */
if
(
fd
!=
-
1
)
{
anet
NonBlock
(
NULL
,
fd
);
anet
EnableTcpNoDelay
(
NULL
,
fd
);
if
(
conn
)
{
conn
NonBlock
(
conn
);
conn
EnableTcpNoDelay
(
conn
);
if
(
server
.
tcpkeepalive
)
anetKeepAlive
(
NULL
,
fd
,
server
.
tcpkeepalive
);
if
(
aeCreateFileEvent
(
server
.
el
,
fd
,
AE_READABLE
,
readQueryFromClient
,
c
)
==
AE_ERR
)
{
close
(
fd
);
zfree
(
c
);
return
NULL
;
}
connKeepAlive
(
conn
,
server
.
tcpkeepalive
);
connSetReadHandler
(
conn
,
readQueryFromClient
);
connSetPrivateData
(
conn
,
c
);
}
selectDb
(
c
,
0
);
uint64_t
client_id
=
++
server
.
next_client_id
;
c
->
id
=
client_id
;
c
->
resp
=
2
;
c
->
fd
=
fd
;
c
->
conn
=
conn
;
c
->
name
=
NULL
;
c
->
bufpos
=
0
;
c
->
qb_pos
=
0
;
...
...
@@ -161,7 +156,7 @@ client *createClient(int fd) {
c
->
client_tracking_redirection
=
0
;
listSetFreeMethod
(
c
->
pubsub_patterns
,
decrRefCountVoid
);
listSetMatchMethod
(
c
->
pubsub_patterns
,
listMatchObjects
);
if
(
fd
!=
-
1
)
linkClient
(
c
);
if
(
conn
)
linkClient
(
c
);
initClientMultiState
(
c
);
return
c
;
}
...
...
@@ -227,7 +222,7 @@ int prepareClientToWrite(client *c) {
if
((
c
->
flags
&
CLIENT_MASTER
)
&&
!
(
c
->
flags
&
CLIENT_MASTER_FORCE_REPLY
))
return
C_ERR
;
if
(
c
->
fd
<=
0
)
return
C_ERR
;
/* Fake client for AOF loading. */
if
(
!
c
->
conn
)
return
C_ERR
;
/* Fake client for AOF loading. */
/* Schedule the client to write the output buffers to the socket, unless
* it should already be setup to do so (it has already pending data). */
...
...
@@ -535,7 +530,7 @@ void addReplyHumanLongDouble(client *c, long double d) {
decrRefCount
(
o
);
}
else
{
char
buf
[
MAX_LONG_DOUBLE_CHARS
];
int
len
=
ld2string
(
buf
,
sizeof
(
buf
),
d
,
1
);
int
len
=
ld2string
(
buf
,
sizeof
(
buf
),
d
,
LD_STR_HUMAN
);
addReplyProto
(
c
,
","
,
1
);
addReplyProto
(
c
,
buf
,
len
);
addReplyProto
(
c
,
"
\r\n
"
,
2
);
...
...
@@ -777,28 +772,13 @@ int clientHasPendingReplies(client *c) {
return
c
->
bufpos
||
listLength
(
c
->
reply
);
}
#define MAX_ACCEPTS_PER_CALL 1000
static
void
acceptCommonHandler
(
int
fd
,
int
flags
,
char
*
ip
)
{
client
*
c
;
if
((
c
=
createClient
(
fd
))
==
NULL
)
{
serverLog
(
LL_WARNING
,
"Error registering fd event for the new client: %s (fd=%d)"
,
strerror
(
errno
),
fd
);
close
(
fd
);
/* May be already closed, just ignore errors */
return
;
}
/* If maxclient directive is set and this is one client more... close the
* connection. Note that we create the client instead to check before
* for this condition, since now the socket is already set in non-blocking
* mode and we can send an error for free using the Kernel I/O */
if
(
listLength
(
server
.
clients
)
>
server
.
maxclients
)
{
char
*
err
=
"-ERR max number of clients reached
\r\n
"
;
void
clientAcceptHandler
(
connection
*
conn
)
{
client
*
c
=
connGetPrivateData
(
conn
);
/* That's a best effort error message, don't check write errors */
if
(
write
(
c
->
fd
,
err
,
strlen
(
err
))
==
-
1
)
{
/* Nothing to do, Just to avoid the warning... */
}
server
.
stat_rejected_conn
++
;
if
(
connGetState
(
conn
)
!=
CONN_STATE_CONNECTED
)
{
serverLog
(
LL_WARNING
,
"Error accepting a client connection: %s"
,
connGetLastError
(
conn
));
freeClient
(
c
);
return
;
}
...
...
@@ -810,10 +790,12 @@ static void acceptCommonHandler(int fd, int flags, char *ip) {
if
(
server
.
protected_mode
&&
server
.
bindaddr_count
==
0
&&
DefaultUser
->
flags
&
USER_FLAG_NOPASS
&&
!
(
flags
&
CLIENT_UNIX_SOCKET
)
&&
ip
!=
NULL
)
!
(
c
->
flags
&
CLIENT_UNIX_SOCKET
))
{
if
(
strcmp
(
ip
,
"127.0.0.1"
)
&&
strcmp
(
ip
,
"::1"
))
{
char
cip
[
NET_IP_STR_LEN
+
1
]
=
{
0
};
connPeerToString
(
conn
,
cip
,
sizeof
(
cip
)
-
1
,
NULL
);
if
(
strcmp
(
cip
,
"127.0.0.1"
)
&&
strcmp
(
cip
,
"::1"
))
{
char
*
err
=
"-DENIED Redis is running in protected mode because protected "
"mode is enabled, no bind address was specified, no "
...
...
@@ -835,7 +817,7 @@ static void acceptCommonHandler(int fd, int flags, char *ip) {
"4) Setup a bind address or an authentication password. "
"NOTE: You only need to do one of the above things in order for "
"the server to start accepting connections from the outside.
\r\n
"
;
if
(
w
rite
(
c
->
fd
,
err
,
strlen
(
err
))
==
-
1
)
{
if
(
connW
rite
(
c
->
conn
,
err
,
strlen
(
err
))
==
-
1
)
{
/* Nothing to do, Just to avoid the warning... */
}
server
.
stat_rejected_conn
++
;
...
...
@@ -845,7 +827,65 @@ static void acceptCommonHandler(int fd, int flags, char *ip) {
}
server
.
stat_numconnections
++
;
moduleFireServerEvent
(
REDISMODULE_EVENT_CLIENT_CHANGE
,
REDISMODULE_SUBEVENT_CLIENT_CHANGE_CONNECTED
,
c
);
}
#define MAX_ACCEPTS_PER_CALL 1000
static
void
acceptCommonHandler
(
connection
*
conn
,
int
flags
,
char
*
ip
)
{
client
*
c
;
UNUSED
(
ip
);
/* Admission control will happen before a client is created and connAccept()
* called, because we don't want to even start transport-level negotiation
* if rejected.
*/
if
(
listLength
(
server
.
clients
)
>=
server
.
maxclients
)
{
char
*
err
=
"-ERR max number of clients reached
\r\n
"
;
/* That's a best effort error message, don't check write errors.
* Note that for TLS connections, no handshake was done yet so nothing is written
* and the connection will just drop.
*/
if
(
connWrite
(
conn
,
err
,
strlen
(
err
))
==
-
1
)
{
/* Nothing to do, Just to avoid the warning... */
}
server
.
stat_rejected_conn
++
;
connClose
(
conn
);
return
;
}
/* Create connection and client */
if
((
c
=
createClient
(
conn
))
==
NULL
)
{
char
conninfo
[
100
];
serverLog
(
LL_WARNING
,
"Error registering fd event for the new client: %s (conn: %s)"
,
connGetLastError
(
conn
),
connGetInfo
(
conn
,
conninfo
,
sizeof
(
conninfo
)));
connClose
(
conn
);
/* May be already closed, just ignore errors */
return
;
}
/* Last chance to keep flags */
c
->
flags
|=
flags
;
/* Initiate accept.
*
* Note that connAccept() is free to do two things here:
* 1. Call clientAcceptHandler() immediately;
* 2. Schedule a future call to clientAcceptHandler().
*
* Because of that, we must do nothing else afterwards.
*/
if
(
connAccept
(
conn
,
clientAcceptHandler
)
==
C_ERR
)
{
char
conninfo
[
100
];
serverLog
(
LL_WARNING
,
"Error accepting a client connection: %s (conn: %s)"
,
connGetLastError
(
conn
),
connGetInfo
(
conn
,
conninfo
,
sizeof
(
conninfo
)));
freeClient
(
connGetPrivateData
(
conn
));
return
;
}
}
void
acceptTcpHandler
(
aeEventLoop
*
el
,
int
fd
,
void
*
privdata
,
int
mask
)
{
...
...
@@ -864,7 +904,27 @@ void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
return
;
}
serverLog
(
LL_VERBOSE
,
"Accepted %s:%d"
,
cip
,
cport
);
acceptCommonHandler
(
cfd
,
0
,
cip
);
acceptCommonHandler
(
connCreateAcceptedSocket
(
cfd
),
0
,
cip
);
}
}
void
acceptTLSHandler
(
aeEventLoop
*
el
,
int
fd
,
void
*
privdata
,
int
mask
)
{
int
cport
,
cfd
,
max
=
MAX_ACCEPTS_PER_CALL
;
char
cip
[
NET_IP_STR_LEN
];
UNUSED
(
el
);
UNUSED
(
mask
);
UNUSED
(
privdata
);
while
(
max
--
)
{
cfd
=
anetTcpAccept
(
server
.
neterr
,
fd
,
cip
,
sizeof
(
cip
),
&
cport
);
if
(
cfd
==
ANET_ERR
)
{
if
(
errno
!=
EWOULDBLOCK
)
serverLog
(
LL_WARNING
,
"Accepting client connection: %s"
,
server
.
neterr
);
return
;
}
serverLog
(
LL_VERBOSE
,
"Accepted %s:%d"
,
cip
,
cport
);
acceptCommonHandler
(
connCreateAcceptedTLS
(
cfd
,
server
.
tls_auth_clients
),
0
,
cip
);
}
}
...
...
@@ -883,7 +943,7 @@ void acceptUnixHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
return
;
}
serverLog
(
LL_VERBOSE
,
"Accepted connection to %s"
,
server
.
unixsocket
);
acceptCommonHandler
(
cfd
,
CLIENT_UNIX_SOCKET
,
NULL
);
acceptCommonHandler
(
connCreateAcceptedSocket
(
cfd
)
,
CLIENT_UNIX_SOCKET
,
NULL
);
}
}
...
...
@@ -914,10 +974,10 @@ void unlinkClient(client *c) {
/* If this is marked as current client unset it. */
if
(
server
.
current_client
==
c
)
server
.
current_client
=
NULL
;
/* Certain operations must be done only if the client has an active
socket
.
/* Certain operations must be done only if the client has an active
connection
.
* If the client was already unlinked or if it's a "fake client" the
*
fd
is already set to
-1
. */
if
(
c
->
fd
!=
-
1
)
{
*
conn
is already set to
NULL
. */
if
(
c
->
conn
)
{
/* Remove from the list of active clients. */
if
(
c
->
client_list_node
)
{
uint64_t
id
=
htonu64
(
c
->
id
);
...
...
@@ -926,21 +986,23 @@ void unlinkClient(client *c) {
c
->
client_list_node
=
NULL
;
}
/* In the case of diskless replication the fork is writing to the
* sockets and just closing the fd isn't enough, if we don't also
* shutdown the socket the fork will continue to write to the slave
* and the salve will only find out that it was disconnected when
* it will finish reading the rdb. */
if
((
c
->
flags
&
CLIENT_SLAVE
)
&&
(
c
->
replstate
==
SLAVE_STATE_WAIT_BGSAVE_END
))
{
shutdown
(
c
->
fd
,
SHUT_RDWR
);
/* Check if this is a replica waiting for diskless replication (rdb pipe),
* in which case it needs to be cleaned from that list */
if
(
c
->
flags
&
CLIENT_SLAVE
&&
c
->
replstate
==
SLAVE_STATE_WAIT_BGSAVE_END
&&
server
.
rdb_pipe_conns
)
{
int
i
;
for
(
i
=
0
;
i
<
server
.
rdb_pipe_numconns
;
i
++
)
{
if
(
server
.
rdb_pipe_conns
[
i
]
==
c
->
conn
)
{
rdbPipeWriteHandlerConnRemoved
(
c
->
conn
);
server
.
rdb_pipe_conns
[
i
]
=
NULL
;
break
;
}
}
/* Unregister async I/O handlers and close the socket. */
aeDeleteFileEvent
(
server
.
el
,
c
->
fd
,
AE_READABLE
);
aeDeleteFileEvent
(
server
.
el
,
c
->
fd
,
AE_WRITABLE
);
close
(
c
->
fd
);
c
->
fd
=
-
1
;
}
connClose
(
c
->
conn
);
c
->
conn
=
NULL
;
}
/* Remove from the list of pending writes if needed. */
...
...
@@ -982,6 +1044,13 @@ void freeClient(client *c) {
return
;
}
/* For connected clients, call the disconnection event of modules hooks. */
if
(
c
->
conn
)
{
moduleFireServerEvent
(
REDISMODULE_EVENT_CLIENT_CHANGE
,
REDISMODULE_SUBEVENT_CLIENT_CHANGE_DISCONNECTED
,
c
);
}
/* If it is our master that's beging disconnected we should make sure
* to cache the state to try a partial resynchronization later.
*
...
...
@@ -1049,6 +1118,11 @@ void freeClient(client *c) {
if
(
c
->
flags
&
CLIENT_SLAVE
&&
listLength
(
server
.
slaves
)
==
0
)
server
.
repl_no_slaves_since
=
server
.
unixtime
;
refreshGoodSlavesCount
();
/* Fire the replica change modules event. */
if
(
c
->
replstate
==
SLAVE_STATE_ONLINE
)
moduleFireServerEvent
(
REDISMODULE_EVENT_REPLICA_CHANGE
,
REDISMODULE_SUBEVENT_REPLICA_CHANGE_OFFLINE
,
NULL
);
}
/* Master/slave cleanup Case 2:
...
...
@@ -1112,19 +1186,20 @@ client *lookupClientByID(uint64_t id) {
/* Write data in output buffers to client. Return C_OK if the client
* is still valid after the call, C_ERR if it was freed because of some
* error.
* error. If handler_installed is set, it will attempt to clear the
* write event.
*
* This function is called by threads, but always with handler_installed
* set to 0. So when handler_installed is set to 0 the function must be
* thread safe. */
int
writeToClient
(
int
fd
,
client
*
c
,
int
handler_installed
)
{
int
writeToClient
(
client
*
c
,
int
handler_installed
)
{
ssize_t
nwritten
=
0
,
totwritten
=
0
;
size_t
objlen
;
clientReplyBlock
*
o
;
while
(
clientHasPendingReplies
(
c
))
{
if
(
c
->
bufpos
>
0
)
{
nwritten
=
write
(
fd
,
c
->
buf
+
c
->
sentlen
,
c
->
bufpos
-
c
->
sentlen
);
nwritten
=
connWrite
(
c
->
conn
,
c
->
buf
+
c
->
sentlen
,
c
->
bufpos
-
c
->
sentlen
);
if
(
nwritten
<=
0
)
break
;
c
->
sentlen
+=
nwritten
;
totwritten
+=
nwritten
;
...
...
@@ -1145,7 +1220,7 @@ int writeToClient(int fd, client *c, int handler_installed) {
continue
;
}
nwritten
=
write
(
fd
,
o
->
buf
+
c
->
sentlen
,
objlen
-
c
->
sentlen
);
nwritten
=
connWrite
(
c
->
conn
,
o
->
buf
+
c
->
sentlen
,
objlen
-
c
->
sentlen
);
if
(
nwritten
<=
0
)
break
;
c
->
sentlen
+=
nwritten
;
totwritten
+=
nwritten
;
...
...
@@ -1180,11 +1255,11 @@ int writeToClient(int fd, client *c, int handler_installed) {
}
server
.
stat_net_output_bytes
+=
totwritten
;
if
(
nwritten
==
-
1
)
{
if
(
errno
==
EAGAIN
)
{
if
(
connGetState
(
c
->
conn
)
==
CONN_STATE_CONNECTED
)
{
nwritten
=
0
;
}
else
{
serverLog
(
LL_VERBOSE
,
"Error writing to client: %s"
,
strerror
(
errno
));
"Error writing to client: %s"
,
connGetLastError
(
c
->
conn
));
freeClientAsync
(
c
);
return
C_ERR
;
}
...
...
@@ -1202,7 +1277,7 @@ int writeToClient(int fd, client *c, int handler_installed) {
* adDeleteFileEvent() is not thread safe: however writeToClient()
* is always called with handler_installed set to 0 from threads
* so we are fine. */
if
(
handler_installed
)
aeDeleteFileEvent
(
server
.
el
,
c
->
fd
,
AE_WRITABLE
);
if
(
handler_installed
)
connSetWriteHandler
(
c
->
conn
,
NULL
);
/* Close connection after entire reply has been sent. */
if
(
c
->
flags
&
CLIENT_CLOSE_AFTER_REPLY
)
{
...
...
@@ -1214,10 +1289,9 @@ int writeToClient(int fd, client *c, int handler_installed) {
}
/* Write event handler. Just send data to the client. */
void
sendReplyToClient
(
aeEventLoop
*
el
,
int
fd
,
void
*
privdata
,
int
mask
)
{
UNUSED
(
el
);
UNUSED
(
mask
);
writeToClient
(
fd
,
privdata
,
1
);
void
sendReplyToClient
(
connection
*
conn
)
{
client
*
c
=
connGetPrivateData
(
conn
);
writeToClient
(
c
,
1
);
}
/* This function is called just before entering the event loop, in the hope
...
...
@@ -1240,25 +1314,23 @@ int handleClientsWithPendingWrites(void) {
if
(
c
->
flags
&
CLIENT_PROTECTED
)
continue
;
/* Try to write buffers to the client socket. */
if
(
writeToClient
(
c
->
fd
,
c
,
0
)
==
C_ERR
)
continue
;
if
(
writeToClient
(
c
,
0
)
==
C_ERR
)
continue
;
/* If after the synchronous writes above we still have data to
* output to the client, we need to install the writable handler. */
if
(
clientHasPendingReplies
(
c
))
{
int
ae_
flags
=
AE_WRITABLE
;
int
ae_
barrier
=
0
;
/* For the fsync=always policy, we want that a given FD is never
* served for reading and writing in the same event loop iteration,
* so that in the middle of receiving the query, and serving it
* to the client, we'll call beforeSleep() that will do the
* actual fsync of AOF to disk.
AE_BARRIER
ensures that. */
* actual fsync of AOF to disk.
the write barrier
ensures that. */
if
(
server
.
aof_state
==
AOF_ON
&&
server
.
aof_fsync
==
AOF_FSYNC_ALWAYS
)
{
ae_
flags
|=
AE_BARRIER
;
ae_
barrier
=
1
;
}
if
(
aeCreateFileEvent
(
server
.
el
,
c
->
fd
,
ae_flags
,
sendReplyToClient
,
c
)
==
AE_ERR
)
{
if
(
connSetWriteHandlerWithBarrier
(
c
->
conn
,
sendReplyToClient
,
ae_barrier
)
==
C_ERR
)
{
freeClientAsync
(
c
);
}
}
...
...
@@ -1305,15 +1377,15 @@ void resetClient(client *c) {
* path, it is not really released, but only marked for later release. */
void
protectClient
(
client
*
c
)
{
c
->
flags
|=
CLIENT_PROTECTED
;
aeDeleteFileEvent
(
server
.
el
,
c
->
fd
,
AE_READABLE
);
aeDeleteFileEvent
(
server
.
el
,
c
->
fd
,
AE_WRITABLE
);
connSetReadHandler
(
c
->
conn
,
NULL
);
connSetWriteHandler
(
c
->
conn
,
NULL
);
}
/* This will undo the client protection done by protectClient() */
void
unprotectClient
(
client
*
c
)
{
if
(
c
->
flags
&
CLIENT_PROTECTED
)
{
c
->
flags
&=
~
CLIENT_PROTECTED
;
aeCreateFileEvent
(
server
.
el
,
c
->
fd
,
AE_READABLE
,
readQueryFromClient
,
c
);
connSetReadHandler
(
c
->
conn
,
readQueryFromClient
);
if
(
clientHasPendingReplies
(
c
))
clientInstallWriteHandler
(
c
);
}
}
...
...
@@ -1710,12 +1782,10 @@ void processInputBufferAndReplicate(client *c) {
}
}
void
readQueryFromClient
(
aeEventLoop
*
el
,
int
fd
,
void
*
privdata
,
int
mask
)
{
client
*
c
=
(
client
*
)
privdata
;
void
readQueryFromClient
(
connection
*
conn
)
{
client
*
c
=
connGetPrivateData
(
conn
)
;
int
nread
,
readlen
;
size_t
qblen
;
UNUSED
(
el
);
UNUSED
(
mask
);
/* Check if we want to read from the client later when exiting from
* the event loop. This is the case if threaded I/O is enabled. */
...
...
@@ -1741,12 +1811,12 @@ void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
qblen
=
sdslen
(
c
->
querybuf
);
if
(
c
->
querybuf_peak
<
qblen
)
c
->
querybuf_peak
=
qblen
;
c
->
querybuf
=
sdsMakeRoomFor
(
c
->
querybuf
,
readlen
);
nread
=
read
(
fd
,
c
->
querybuf
+
qblen
,
readlen
);
nread
=
connRead
(
c
->
conn
,
c
->
querybuf
+
qblen
,
readlen
);
if
(
nread
==
-
1
)
{
if
(
errno
==
EAGAIN
)
{
if
(
connGetState
(
conn
)
==
CONN_STATE_CONNECTED
)
{
return
;
}
else
{
serverLog
(
LL_VERBOSE
,
"Reading from client: %s"
,
strerror
(
errno
));
serverLog
(
LL_VERBOSE
,
"Reading from client: %s"
,
connGetLastError
(
c
->
conn
));
freeClientAsync
(
c
);
return
;
}
...
...
@@ -1818,7 +1888,7 @@ void genClientPeerId(client *client, char *peerid,
snprintf
(
peerid
,
peerid_len
,
"%s:0"
,
server
.
unixsocket
);
}
else
{
/* TCP client. */
anet
FormatPeer
(
client
->
fd
,
peerid
,
peerid_len
);
conn
FormatPeer
(
client
->
conn
,
peerid
,
peerid_len
);
}
}
...
...
@@ -1839,8 +1909,7 @@ char *getClientPeerId(client *c) {
/* Concatenate a string representing the state of a client in an human
* readable format, into the sds string 's'. */
sds
catClientInfoString
(
sds
s
,
client
*
client
)
{
char
flags
[
16
],
events
[
3
],
*
p
;
int
emask
;
char
flags
[
16
],
events
[
3
],
conninfo
[
CONN_INFO_LEN
],
*
p
;
p
=
flags
;
if
(
client
->
flags
&
CLIENT_SLAVE
)
{
...
...
@@ -1864,16 +1933,17 @@ sds catClientInfoString(sds s, client *client) {
if
(
p
==
flags
)
*
p
++
=
'N'
;
*
p
++
=
'\0'
;
emask
=
client
->
fd
==
-
1
?
0
:
aeGetFileEvents
(
server
.
el
,
client
->
fd
);
p
=
events
;
if
(
emask
&
AE_READABLE
)
*
p
++
=
'r'
;
if
(
emask
&
AE_WRITABLE
)
*
p
++
=
'w'
;
if
(
client
->
conn
)
{
if
(
connHasReadHandler
(
client
->
conn
))
*
p
++
=
'r'
;
if
(
connHasWriteHandler
(
client
->
conn
))
*
p
++
=
'w'
;
}
*
p
=
'\0'
;
return
sdscatfmt
(
s
,
"id=%U addr=%s
fd=%i
name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U obl=%U oll=%U omem=%U events=%s cmd=%s user=%s"
,
"id=%U addr=%s
%s
name=%s age=%I idle=%I flags=%s db=%i sub=%i psub=%i multi=%i qbuf=%U qbuf-free=%U obl=%U oll=%U omem=%U events=%s cmd=%s user=%s"
,
(
unsigned
long
long
)
client
->
id
,
getClientPeerId
(
client
),
c
lient
->
fd
,
c
onnGetInfo
(
client
->
conn
,
conninfo
,
sizeof
(
conninfo
))
,
client
->
name
?
(
char
*
)
client
->
name
->
ptr
:
""
,
(
long
long
)(
server
.
unixtime
-
client
->
ctime
),
(
long
long
)(
server
.
unixtime
-
client
->
lastinteraction
),
...
...
@@ -1990,7 +2060,7 @@ NULL
return
;
}
sds
o
=
getAllClientsInfoString
(
type
);
addReply
BulkCBuffer
(
c
,
o
,
sdslen
(
o
));
addReply
Verbatim
(
c
,
o
,
sdslen
(
o
)
,
"txt"
);
sdsfree
(
o
);
}
else
if
(
!
strcasecmp
(
c
->
argv
[
1
]
->
ptr
,
"reply"
)
&&
c
->
argc
==
3
)
{
/* CLIENT REPLY ON|OFF|SKIP */
...
...
@@ -2445,7 +2515,7 @@ int checkClientOutputBufferLimits(client *c) {
* called from contexts where the client can't be freed safely, i.e. from the
* lower level functions pushing data inside the client output buffers. */
void
asyncCloseClientOnOutputBufferLimitReached
(
client
*
c
)
{
if
(
c
->
fd
==
-
1
)
return
;
/* It is unsafe to free fake clients. */
if
(
!
c
->
conn
)
return
;
/* It is unsafe to free fake clients. */
serverAssert
(
c
->
reply_bytes
<
SIZE_MAX
-
(
1024
*
64
));
if
(
c
->
reply_bytes
==
0
||
c
->
flags
&
CLIENT_CLOSE_ASAP
)
return
;
if
(
checkClientOutputBufferLimits
(
c
))
{
...
...
@@ -2468,20 +2538,29 @@ void flushSlavesOutputBuffers(void) {
listRewind
(
server
.
slaves
,
&
li
);
while
((
ln
=
listNext
(
&
li
)))
{
client
*
slave
=
listNodeValue
(
ln
);
int
events
;
/* Note that the following will not flush output buffers of slaves
* in STATE_ONLINE but having put_online_on_ack set to true: in this
* case the writable event is never installed, since the purpose
* of put_online_on_ack is to postpone the moment it is installed.
* This is what we want since slaves in this state should not receive
* writes before the first ACK. */
events
=
aeGetFileEvents
(
server
.
el
,
slave
->
fd
);
if
(
events
&
AE_WRITABLE
&&
slave
->
replstate
==
SLAVE_STATE_ONLINE
&&
int
can_receive_writes
=
connHasWriteHandler
(
slave
->
conn
)
||
(
slave
->
flags
&
CLIENT_PENDING_WRITE
);
/* We don't want to send the pending data to the replica in a few
* cases:
*
* 1. For some reason there is neither the write handler installed
* nor the client is flagged as to have pending writes: for some
* reason this replica may not be set to receive data. This is
* just for the sake of defensive programming.
*
* 2. The put_online_on_ack flag is true. To know why we don't want
* to send data to the replica in this case, please grep for the
* flag for this flag.
*
* 3. Obviously if the slave is not ONLINE.
*/
if
(
slave
->
replstate
==
SLAVE_STATE_ONLINE
&&
can_receive_writes
&&
!
slave
->
repl_put_online_on_ack
&&
clientHasPendingReplies
(
slave
))
{
writeToClient
(
slave
->
fd
,
slave
,
0
);
writeToClient
(
slave
,
0
);
}
}
}
...
...
@@ -2608,9 +2687,9 @@ void *IOThreadMain(void *myid) {
while
((
ln
=
listNext
(
&
li
)))
{
client
*
c
=
listNodeValue
(
ln
);
if
(
io_threads_op
==
IO_THREADS_OP_WRITE
)
{
writeToClient
(
c
->
fd
,
c
,
0
);
writeToClient
(
c
,
0
);
}
else
if
(
io_threads_op
==
IO_THREADS_OP_READ
)
{
readQueryFromClient
(
NULL
,
c
->
fd
,
c
,
0
);
readQueryFromClient
(
c
->
conn
);
}
else
{
serverPanic
(
"io_threads_op value is unknown"
);
}
...
...
@@ -2751,8 +2830,7 @@ int handleClientsWithPendingWritesUsingThreads(void) {
/* Install the write handler if there are pending writes in some
* of the clients. */
if
(
clientHasPendingReplies
(
c
)
&&
aeCreateFileEvent
(
server
.
el
,
c
->
fd
,
AE_WRITABLE
,
sendReplyToClient
,
c
)
==
AE_ERR
)
connSetWriteHandler
(
c
->
conn
,
sendReplyToClient
)
==
AE_ERR
)
{
freeClientAsync
(
c
);
}
...
...
src/object.c
View file @
2968d8e3
...
...
@@ -178,7 +178,7 @@ robj *createStringObjectFromLongLongForValue(long long value) {
* The 'humanfriendly' option is used for INCRBYFLOAT and HINCRBYFLOAT. */
robj
*
createStringObjectFromLongDouble
(
long
double
value
,
int
humanfriendly
)
{
char
buf
[
MAX_LONG_DOUBLE_CHARS
];
int
len
=
ld2string
(
buf
,
sizeof
(
buf
),
value
,
humanfriendly
);
int
len
=
ld2string
(
buf
,
sizeof
(
buf
),
value
,
humanfriendly
?
LD_STR_HUMAN
:
LD_STR_AUTO
);
return
createStringObject
(
buf
,
len
);
}
...
...
@@ -467,10 +467,15 @@ robj *tryObjectEncoding(robj *o) {
incrRefCount
(
shared
.
integers
[
value
]);
return
shared
.
integers
[
value
];
}
else
{
if
(
o
->
encoding
==
OBJ_ENCODING_RAW
)
sdsfree
(
o
->
ptr
);
if
(
o
->
encoding
==
OBJ_ENCODING_RAW
)
{
sdsfree
(
o
->
ptr
);
o
->
encoding
=
OBJ_ENCODING_INT
;
o
->
ptr
=
(
void
*
)
value
;
return
o
;
}
else
if
(
o
->
encoding
==
OBJ_ENCODING_EMBSTR
)
{
decrRefCount
(
o
);
return
createStringObjectFromLongLongForValue
(
value
);
}
}
}
...
...
@@ -601,21 +606,13 @@ size_t stringObjectLen(robj *o) {
int
getDoubleFromObject
(
const
robj
*
o
,
double
*
target
)
{
double
value
;
char
*
eptr
;
if
(
o
==
NULL
)
{
value
=
0
;
}
else
{
serverAssertWithInfo
(
NULL
,
o
,
o
->
type
==
OBJ_STRING
);
if
(
sdsEncodedObject
(
o
))
{
errno
=
0
;
value
=
strtod
(
o
->
ptr
,
&
eptr
);
if
(
sdslen
(
o
->
ptr
)
==
0
||
isspace
(((
const
char
*
)
o
->
ptr
)[
0
])
||
(
size_t
)(
eptr
-
(
char
*
)
o
->
ptr
)
!=
sdslen
(
o
->
ptr
)
||
(
errno
==
ERANGE
&&
(
value
==
HUGE_VAL
||
value
==
-
HUGE_VAL
||
value
==
0
))
||
isnan
(
value
))
if
(
!
string2d
(
o
->
ptr
,
sdslen
(
o
->
ptr
),
&
value
))
return
C_ERR
;
}
else
if
(
o
->
encoding
==
OBJ_ENCODING_INT
)
{
value
=
(
long
)
o
->
ptr
;
...
...
@@ -1204,19 +1201,20 @@ sds getMemoryDoctorReport(void) {
* The lru_idle and lru_clock args are only relevant if policy
* is MAXMEMORY_FLAG_LRU.
* Either or both of them may be <0, in that case, nothing is set. */
void
objectSetLRUOrLFU
(
robj
*
val
,
long
long
lfu_freq
,
long
long
lru_idle
,
long
long
lru_clock
)
{
int
objectSetLRUOrLFU
(
robj
*
val
,
long
long
lfu_freq
,
long
long
lru_idle
,
long
long
lru_clock
,
int
lru_multiplier
)
{
if
(
server
.
maxmemory_policy
&
MAXMEMORY_FLAG_LFU
)
{
if
(
lfu_freq
>=
0
)
{
serverAssert
(
lfu_freq
<=
255
);
val
->
lru
=
(
LFUGetTimeInMinutes
()
<<
8
)
|
lfu_freq
;
return
1
;
}
}
else
if
(
lru_idle
>=
0
)
{
/* Provided LRU idle time is in seconds. Scale
* according to the LRU clock resolution this Redis
* instance was compiled with (normally 1000 ms, so the
* below statement will expand to lru_idle*1000/1000. */
lru_idle
=
lru_idle
*
1000
/
LRU_CLOCK_RESOLUTION
;
lru_idle
=
lru_idle
*
lru_multiplier
/
LRU_CLOCK_RESOLUTION
;
long
lru_abs
=
lru_clock
-
lru_idle
;
/* Absolute access time. */
/* If the LRU field underflows (since LRU it is a wrapping
* clock), the best we can do is to provide a large enough LRU
...
...
@@ -1226,7 +1224,9 @@ void objectSetLRUOrLFU(robj *val, long long lfu_freq, long long lru_idle,
if
(
lru_abs
<
0
)
lru_abs
=
(
lru_clock
+
(
LRU_CLOCK_MAX
/
2
))
%
LRU_CLOCK_MAX
;
val
->
lru
=
lru_abs
;
return
1
;
}
return
0
;
}
/* ======================= The OBJECT and MEMORY commands =================== */
...
...
@@ -1435,30 +1435,20 @@ NULL
#if defined(USE_JEMALLOC)
sds
info
=
sdsempty
();
je_malloc_stats_print
(
inputCatSds
,
&
info
,
NULL
);
addReplyBulkSds
(
c
,
info
);
addReplyVerbatim
(
c
,
info
,
sdslen
(
info
),
"txt"
);
sdsfree
(
info
);
#else
addReplyBulkCString
(
c
,
"Stats not supported for the current allocator"
);
#endif
}
else
if
(
!
strcasecmp
(
c
->
argv
[
1
]
->
ptr
,
"doctor"
)
&&
c
->
argc
==
2
)
{
sds
report
=
getMemoryDoctorReport
();
addReplyBulkSds
(
c
,
report
);
addReplyVerbatim
(
c
,
report
,
sdslen
(
report
),
"txt"
);
sdsfree
(
report
);
}
else
if
(
!
strcasecmp
(
c
->
argv
[
1
]
->
ptr
,
"purge"
)
&&
c
->
argc
==
2
)
{
#if defined(USE_JEMALLOC)
char
tmp
[
32
];
unsigned
narenas
=
0
;
size_t
sz
=
sizeof
(
unsigned
);
if
(
!
je_mallctl
(
"arenas.narenas"
,
&
narenas
,
&
sz
,
NULL
,
0
))
{
sprintf
(
tmp
,
"arena.%d.purge"
,
narenas
);
if
(
!
je_mallctl
(
tmp
,
NULL
,
0
,
NULL
,
0
))
{
if
(
jemalloc_purge
()
==
0
)
addReply
(
c
,
shared
.
ok
);
return
;
}
}
else
addReplyError
(
c
,
"Error purging dirty pages"
);
#else
addReply
(
c
,
shared
.
ok
);
/* Nothing to do for other allocators. */
#endif
}
else
{
addReplyErrorFormat
(
c
,
"Unknown subcommand or wrong number of arguments for '%s'. Try MEMORY HELP"
,
(
char
*
)
c
->
argv
[
1
]
->
ptr
);
}
...
...
Prev
1
2
3
4
5
6
7
8
9
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment