Skip to content

Commit f6f2d25

Browse files
clerouxalicebob
authored andcommitted
Support for TOUCH command
1 parent 3f1597e commit f6f2d25

File tree

8 files changed

+161
-0
lines changed

8 files changed

+161
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Implemented commands:
4242
- RENAMENX
4343
- RANDOMKEY -- see m.Seed(...)
4444
- SCAN
45+
- TOUCH
4546
- TTL
4647
- TYPE
4748
- UNLINK

cmd_generic.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func commandsGeneric(m *Miniredis) {
3131
m.srv.Register("RENAMENX", m.cmdRenamenx)
3232
// RESTORE
3333
// SORT
34+
m.srv.Register("TOUCH", m.cmdTouch)
3435
m.srv.Register("TTL", m.cmdTTL)
3536
m.srv.Register("TYPE", m.cmdType)
3637
m.srv.Register("SCAN", m.cmdScan)
@@ -85,13 +86,43 @@ func makeCmdExpire(m *Miniredis, unix bool, d time.Duration) func(*server.Peer,
8586
} else {
8687
db.ttl[key] = time.Duration(i) * d
8788
}
89+
db.origTtl[key] = db.ttl[key]
8890
db.keyVersion[key]++
8991
db.checkTTL(key)
9092
c.WriteInt(1)
9193
})
9294
}
9395
}
9496

97+
// TOUCH
98+
func (m *Miniredis) cmdTouch(c *server.Peer, cmd string, args []string) {
99+
if !m.handleAuth(c) {
100+
return
101+
}
102+
if m.checkPubsub(c) {
103+
return
104+
}
105+
106+
if len(args) == 0 {
107+
setDirty(c)
108+
c.WriteError(errWrongNumber(cmd))
109+
return
110+
}
111+
112+
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
113+
db := m.db(ctx.selectedDB)
114+
115+
count := 0
116+
for _, key := range args {
117+
if db.exists(key) {
118+
count++
119+
db.touch(key)
120+
}
121+
}
122+
c.WriteInt(count)
123+
})
124+
}
125+
95126
// TTL
96127
func (m *Miniredis) cmdTTL(c *server.Peer, cmd string, args []string) {
97128
if len(args) != 1 {
@@ -192,6 +223,7 @@ func (m *Miniredis) cmdPersist(c *server.Peer, cmd string, args []string) {
192223
c.WriteInt(0)
193224
return
194225
}
226+
delete(db.origTtl, key)
195227
delete(db.ttl, key)
196228
db.keyVersion[key]++
197229
c.WriteInt(1)

cmd_generic_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,85 @@ func TestExpireat(t *testing.T) {
143143
}
144144
}
145145

146+
func TestTouch(t *testing.T) {
147+
s, err := Run()
148+
ok(t, err)
149+
defer s.Close()
150+
c, err := redis.Dial("tcp", s.Addr())
151+
ok(t, err)
152+
153+
// Set something
154+
t.Run("basic", func(t *testing.T) {
155+
s.SetTime(time.Unix(1234567890, 0))
156+
_, err := c.Do("SET", "foo", "bar", "EX", 100)
157+
ok(t, err)
158+
_, err = c.Do("SET", "baz", "qux", "EX", 100)
159+
ok(t, err)
160+
161+
// Advance time, keys still exist with 1 second TTL
162+
s.FastForward(time.Second * 99)
163+
equals(t, time.Second, s.TTL("foo"))
164+
equals(t, time.Second, s.TTL("baz"))
165+
166+
// Change TTL on a key to test that TOUCH will use the new value
167+
_, err = c.Do("EXPIRE", "foo", "200")
168+
ok(t, err)
169+
170+
// Touch one key
171+
n, err := redis.Int(c.Do("TOUCH", "baz"))
172+
ok(t, err)
173+
equals(t, 1, n)
174+
175+
s.FastForward(time.Second * 99)
176+
equals(t, time.Second*101, s.TTL("foo"))
177+
equals(t, time.Second, s.TTL("baz"))
178+
179+
// Reset TTL on multiple keys, "nay" doesn't exist
180+
n, err = redis.Int(c.Do("TOUCH", "foo", "baz", "nay"))
181+
ok(t, err)
182+
equals(t, 2, n)
183+
184+
equals(t, time.Second*200, s.TTL("foo"))
185+
equals(t, time.Second*100, s.TTL("baz"))
186+
})
187+
188+
t.Run("rename", func(t *testing.T) {
189+
s.SetTime(time.Unix(1234567890, 0))
190+
_, err := c.Do("SET", "foo", "bar", "EX", 100)
191+
ok(t, err)
192+
n, err := redis.Int(c.Do("TOUCH", "foo"))
193+
ok(t, err)
194+
equals(t, 1, n)
195+
196+
s.FastForward(time.Second * 60)
197+
equals(t, 40*time.Second, s.TTL("foo"))
198+
199+
_, err = redis.String(c.Do("RENAME", "foo", "foo2"))
200+
ok(t, err)
201+
n, err = redis.Int(c.Do("TOUCH", "foo2"))
202+
ok(t, err)
203+
equals(t, 1, n)
204+
equals(t, 100*time.Second, s.TTL("foo2"))
205+
})
206+
207+
t.Run("failure cases", func(t *testing.T) {
208+
_, err := c.Do("TOUCH")
209+
mustFail(t, err, "ERR wrong number of arguments for 'touch' command")
210+
})
211+
212+
t.Run("direct", func(t *testing.T) {
213+
s.Set("dir", "bar")
214+
s.SetTTL("dir", 30*time.Second)
215+
equals(t, 30*time.Second, s.TTL("dir"))
216+
217+
s.FastForward(10 * time.Second)
218+
equals(t, 20*time.Second, s.TTL("dir"))
219+
220+
s.Touch("dir")
221+
equals(t, 30*time.Second, s.TTL("dir"))
222+
})
223+
}
224+
146225
func TestPexpireat(t *testing.T) {
147226
s, err := Run()
148227
ok(t, err)

cmd_string.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func (m *Miniredis) cmdSet(c *server.Peer, cmd string, args []string) {
121121
// a vanilla SET clears the expire
122122
db.stringSet(key, value)
123123
if ttl != 0 {
124+
db.origTtl[key] = ttl
124125
db.ttl[key] = ttl
125126
}
126127
c.WriteOK()
@@ -161,6 +162,7 @@ func (m *Miniredis) cmdSetex(c *server.Peer, cmd string, args []string) {
161162
db.del(key, true) // Clear any existing keys.
162163
db.stringSet(key, value)
163164
db.ttl[key] = time.Duration(ttl) * time.Second
165+
db.origTtl[key] = db.ttl[key]
164166
c.WriteOK()
165167
})
166168
}
@@ -199,6 +201,7 @@ func (m *Miniredis) cmdPsetex(c *server.Peer, cmd string, args []string) {
199201
db.del(key, true) // Clear any existing keys.
200202
db.stringSet(key, value)
201203
db.ttl[key] = time.Duration(ttl) * time.Millisecond
204+
db.origTtl[key] = db.ttl[key]
202205
c.WriteOK()
203206
})
204207
}
@@ -374,6 +377,7 @@ func (m *Miniredis) cmdGetset(c *server.Peer, cmd string, args []string) {
374377
old, ok := db.stringKeys[key]
375378
db.stringSet(key, value)
376379
// a GETSET clears the ttl
380+
delete(db.origTtl, key)
377381
delete(db.ttl, key)
378382

379383
if !ok {

db.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func (db *RedisDB) flush() {
4141
db.listKeys = map[string]listKey{}
4242
db.setKeys = map[string]setKey{}
4343
db.sortedsetKeys = map[string]sortedSet{}
44+
db.origTtl = map[string]time.Duration{}
4445
db.ttl = map[string]time.Duration{}
4546
}
4647

@@ -102,6 +103,10 @@ func (db *RedisDB) rename(from, to string) {
102103
if v, ok := db.ttl[from]; ok {
103104
db.ttl[to] = v
104105
}
106+
if v, ok := db.origTtl[from]; ok {
107+
delete(db.origTtl, from)
108+
db.origTtl[to] = v
109+
}
105110

106111
db.del(from, true)
107112
}
@@ -114,6 +119,7 @@ func (db *RedisDB) del(k string, delTTL bool) {
114119
delete(db.keys, k)
115120
db.keyVersion[k]++
116121
if delTTL {
122+
delete(db.origTtl, k)
117123
delete(db.ttl, k)
118124
}
119125
switch t {
@@ -803,3 +809,8 @@ func (db *RedisDB) checkTTL(key string) {
803809
db.del(key, true)
804810
}
805811
}
812+
813+
// Touch resets a key's TTL to its original unmodified value.
814+
func (db *RedisDB) touch(k string) {
815+
db.ttl[k] = db.origTtl[k]
816+
}

direct.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,23 @@ func (db *RedisDB) SetTTL(k string, ttl time.Duration) {
416416
defer db.master.Unlock()
417417
defer db.master.signal.Broadcast()
418418

419+
db.origTtl[k] = ttl
419420
db.ttl[k] = ttl
420421
db.keyVersion[k]++
421422
}
422423

424+
// Touch resets the TTL of the key
425+
func (m *Miniredis) Touch(k string) {
426+
m.DB(m.selectedDB).Touch(k)
427+
}
428+
429+
func (db *RedisDB) Touch(k string) {
430+
db.master.Lock()
431+
defer db.master.Unlock()
432+
433+
db.touch(k)
434+
}
435+
423436
// Type gives the type of a key, or ""
424437
func (m *Miniredis) Type(k string) string {
425438
return m.DB(m.selectedDB).Type(k)

integration/generic_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,22 @@ func TestUnlink(t *testing.T) {
279279
succSorted("KEYS", "*"),
280280
)
281281
}
282+
283+
func TestTouch(t *testing.T) {
284+
testCommands(t,
285+
succ("SET", "a", "some value"),
286+
succ("TTL", "a"),
287+
succ("EXPIRE", "a", 400),
288+
succ("TTL", "a"),
289+
290+
succ("TOUCH", "a"),
291+
succ("TTL", "a"),
292+
succ("TOUCH", "a", "foobar", "a"),
293+
294+
succ("RENAME", "a", "a2"),
295+
succ("TOUCH", "a"),
296+
succ("TTL", "a"), // hard to test
297+
298+
fail("TOUCH"),
299+
)
300+
}

miniredis.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type RedisDB struct {
4141
sortedsetKeys map[string]sortedSet // ZADD &c. keys
4242
streamKeys map[string]streamKey // XADD &c. keys
4343
streamGroupKeys map[string]streamGroupKey // XREADGROUP &c. keys
44+
origTtl map[string]time.Duration // unmodified TTL values
4445
ttl map[string]time.Duration // effective TTL values
4546
keyVersion map[string]uint // used to watch values
4647
}
@@ -105,6 +106,7 @@ func newRedisDB(id int, m *Miniredis) RedisDB {
105106
sortedsetKeys: map[string]sortedSet{},
106107
streamKeys: map[string]streamKey{},
107108
streamGroupKeys: map[string]streamGroupKey{},
109+
origTtl: map[string]time.Duration{},
108110
ttl: map[string]time.Duration{},
109111
keyVersion: map[string]uint{},
110112
}

0 commit comments

Comments
 (0)