Skip to content

Commit f68d6b2

Browse files
committed
Initial commit of disklru
0 parents  commit f68d6b2

17 files changed

Lines changed: 805 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Test
2+
on: [pull_request]
3+
4+
jobs:
5+
build:
6+
name: Test
7+
steps:
8+
- name: Check out code into the Go module directory
9+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
10+
with:
11+
submodules: "true"
12+
13+
- name: Set up Go 1.25
14+
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe
15+
with:
16+
go-version: 1.25
17+
18+
# Caching seems to really slow down the build due to the time
19+
# taken to save the cache.
20+
cache: false
21+
id: go
22+
23+
- name: Test
24+
run: |
25+
make test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
inspector

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
all:
2+
go build -o ./inspector ./cmd
3+
4+
test:
5+
go test -v ./...

api.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Based on https://github.com/jkelin/cache-sqlite-lru-ttl/
2+
3+
package disklru
4+
5+
import (
6+
"context"
7+
"database/sql"
8+
"time"
9+
10+
_ "github.com/mattn/go-sqlite3"
11+
)
12+
13+
type Encoder interface {
14+
Encode(obj interface{}) ([]byte, error)
15+
Decode(in []byte) (interface{}, error)
16+
}
17+
18+
type DiskLRU struct {
19+
handle *sql.DB
20+
encoder Encoder
21+
22+
set_stm, get_stm, get_update_expiry_stm,
23+
peek_stm, delete_stm,
24+
clear_stm, cleanup_expires_stm,
25+
cleanup_lru_stm *sql.Stmt
26+
27+
opts Options
28+
29+
hit, miss Counter
30+
31+
cancel func()
32+
}
33+
34+
func (self *DiskLRU) HouseKeepOnce() {
35+
self.Debug("Housekeeping run")
36+
37+
self.cleanup_lru_stm.Exec(_max_items(self))
38+
self.cleanup_expires_stm.Exec(_now(self))
39+
}
40+
41+
func (self *DiskLRU) houseKeeping(ctx context.Context) {
42+
if self.opts.HouseKeepPeriodSec < 0 {
43+
return
44+
}
45+
46+
for {
47+
select {
48+
case <-ctx.Done():
49+
return
50+
51+
case <-time.After(time.Second *
52+
time.Duration(self.opts.HouseKeepPeriodSec)):
53+
self.HouseKeepOnce()
54+
}
55+
}
56+
}
57+
58+
func (self *DiskLRU) Close() error {
59+
self.Debug("Close")
60+
self.cancel()
61+
self.set_stm.Close()
62+
self.get_stm.Close()
63+
self.get_update_expiry_stm.Close()
64+
self.peek_stm.Close()
65+
self.delete_stm.Close()
66+
self.clear_stm.Close()
67+
self.cleanup_lru_stm.Close()
68+
self.cleanup_expires_stm.Close()
69+
70+
return self.handle.Close()
71+
}
72+
73+
func (self *DiskLRU) initTables() error {
74+
_, err := self.handle.Exec(`
75+
PRAGMA journal_mode=WAL;
76+
PRAGMA busy_timeout=60000;
77+
PRAGMA synchronous=NORMAL;
78+
79+
CREATE TABLE IF NOT EXISTS cache (
80+
key TEXT PRIMARY KEY,
81+
value BLOB,
82+
expires BIGINT,
83+
lastAccess BIGINT
84+
);
85+
86+
CREATE UNIQUE INDEX IF NOT EXISTS key ON cache (key);
87+
CREATE INDEX IF NOT EXISTS expires ON cache (expires);
88+
CREATE INDEX IF NOT EXISTS lastAccess ON cache (lastAccess);
89+
90+
`)
91+
return err
92+
}
93+
94+
func (self *DiskLRU) Set(key string, value interface{}) error {
95+
buf, err := self.encoder.Encode(value)
96+
if err != nil {
97+
return err
98+
}
99+
100+
_, err = self.set_stm.Exec(
101+
_now(self), _key(key), _value(buf), _expires(self))
102+
self.Debug("key %v set %v error: %v", key, string(buf), err)
103+
104+
return err
105+
}
106+
107+
func (self *DiskLRU) Get(key string) (interface{}, error) {
108+
var buf []byte
109+
110+
if self.opts.UpdateExpiryOnAccess {
111+
err := self.get_update_expiry_stm.QueryRow(
112+
_now(self), _key(key), _expires(self)).Scan(&buf)
113+
if err != nil {
114+
self.miss.Inc()
115+
self.Debug("Get Key not found %v", key)
116+
return nil, KeyNotFoundError
117+
}
118+
119+
} else {
120+
err := self.get_stm.QueryRow(_now(self), _key(key)).Scan(&buf)
121+
if err != nil {
122+
self.miss.Inc()
123+
self.Debug("Get Key not found %v: %v", key, err)
124+
return nil, KeyNotFoundError
125+
}
126+
}
127+
128+
self.hit.Inc()
129+
self.Debug("Get Key %v: %v", key, string(buf))
130+
return self.encoder.Decode(buf)
131+
}
132+
133+
type CacheItem struct {
134+
Key string
135+
Value interface{}
136+
}
137+
138+
func (self *DiskLRU) Items() (res []CacheItem) {
139+
rows, err := self.handle.Query("SELECT key, value FROM cache")
140+
if err != nil {
141+
return
142+
}
143+
defer rows.Close()
144+
145+
for rows.Next() {
146+
var key string
147+
var buf []byte
148+
err := rows.Scan(&key, &buf)
149+
if err != nil {
150+
continue
151+
}
152+
153+
value, err := self.encoder.Decode(buf)
154+
if err != nil {
155+
continue
156+
}
157+
res = append(res, CacheItem{
158+
Key: key,
159+
Value: value,
160+
})
161+
}
162+
163+
self.Debug("Items call returned %v items", len(res))
164+
165+
return res
166+
}
167+
168+
func (self *DiskLRU) Peek(key string) (interface{}, error) {
169+
var buf []byte
170+
171+
err := self.peek_stm.QueryRow(_key(key)).Scan(&buf)
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
return self.encoder.Decode(buf)
177+
}
178+
179+
func (self *DiskLRU) SetEncoder(encoder Encoder) {
180+
self.encoder = encoder
181+
}
182+
183+
func (self *DiskLRU) Delete(key string) bool {
184+
self.Debug("Delete key %v", key)
185+
self.delete_stm.Exec(_key(key))
186+
return false
187+
}
188+
189+
func NewDiskLRU(
190+
ctx context.Context, opts Options) (*DiskLRU, error) {
191+
192+
opts.UpdateDefaults()
193+
194+
handle, err := sql.Open("sqlite3", opts.Filename)
195+
if err != nil {
196+
return nil, err
197+
}
198+
199+
sub_ctx, cancel := context.WithCancel(ctx)
200+
201+
self := &DiskLRU{
202+
handle: handle,
203+
encoder: JsonEncoder{},
204+
opts: opts,
205+
cancel: cancel,
206+
}
207+
208+
err = self.initTables()
209+
if err != nil {
210+
return nil, err
211+
}
212+
213+
self.set_stm, err = handle.Prepare(
214+
`REPLACE INTO cache(key, value, expires, lastAccess)
215+
VALUES (@key, @value, @expires, @now)`)
216+
if err != nil {
217+
return nil, err
218+
}
219+
220+
self.get_stm, err = handle.Prepare(
221+
`UPDATE OR IGNORE cache
222+
SET lastAccess = @now
223+
WHERE key = @key AND (expires > @now OR expires IS NULL)
224+
RETURNING value`)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
self.get_update_expiry_stm, err = handle.Prepare(
230+
`UPDATE OR IGNORE cache
231+
SET lastAccess = @now, expires = @expires
232+
WHERE key = @key AND (expires > @now OR expires IS NULL)
233+
RETURNING value`)
234+
if err != nil {
235+
return nil, err
236+
}
237+
238+
self.peek_stm, err = handle.Prepare(
239+
`SELECT value FROM cache
240+
WHERE key = @key`)
241+
if err != nil {
242+
return nil, err
243+
}
244+
245+
self.delete_stm, err = handle.Prepare(
246+
`DELETE FROM cache WHERE key = @key`)
247+
if err != nil {
248+
return nil, err
249+
}
250+
251+
self.clear_stm, err = handle.Prepare(`DELETE FROM cache`)
252+
if err != nil {
253+
return nil, err
254+
}
255+
256+
self.cleanup_expires_stm, err = handle.Prepare(
257+
`DELETE FROM cache WHERE expires < @now`)
258+
if err != nil {
259+
return nil, err
260+
}
261+
262+
self.cleanup_lru_stm, err = handle.Prepare(
263+
`DELETE FROM cache
264+
WHERE key IN (
265+
SELECT key FROM cache
266+
ORDER BY lastAccess ASC
267+
LIMIT MAX(0, (SELECT COUNT(*) - @maxItems FROM cache)))`)
268+
if err != nil {
269+
return nil, err
270+
}
271+
272+
// Run the housekeep thread
273+
go self.houseKeeping(sub_ctx)
274+
275+
return self, nil
276+
}

cmd/get.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package main
2+
3+
import (
4+
"Velocidex/disklru"
5+
"context"
6+
"fmt"
7+
8+
"github.com/alecthomas/kingpin"
9+
)
10+
11+
var (
12+
get_command = app.Command("get", "Get string from cache.")
13+
get_command_filename = get_command.Arg(
14+
"filename", "Cache filename").Required().String()
15+
16+
get_command_key = get_command.Arg("key", "The key to store").String()
17+
)
18+
19+
func doGet() {
20+
opts := disklru.Options{
21+
Filename: *get_command_filename,
22+
}
23+
cache, err := disklru.NewDiskLRU(context.Background(), opts)
24+
kingpin.FatalIfError(err, "Creating cache")
25+
defer cache.Close()
26+
27+
value, err := cache.Get(*get_command_key)
28+
kingpin.FatalIfError(err, "Getting cache")
29+
30+
fmt.Printf("Value %v\n", value)
31+
}
32+
33+
func init() {
34+
command_handlers = append(command_handlers, func(command string) bool {
35+
switch command {
36+
case get_command.FullCommand():
37+
doGet()
38+
default:
39+
return false
40+
}
41+
return true
42+
})
43+
}

cmd/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/alecthomas/kingpin"
7+
)
8+
9+
type CommandHandler func(command string) bool
10+
11+
var (
12+
app = kingpin.New("lru", "A tool for dump ebpf events.")
13+
14+
command_handlers []CommandHandler
15+
)
16+
17+
func main() {
18+
app.HelpFlag.Short('h')
19+
app.UsageTemplate(kingpin.CompactUsageTemplate)
20+
command := kingpin.MustParse(app.Parse(os.Args[1:]))
21+
22+
for _, command_handler := range command_handlers {
23+
if command_handler(command) {
24+
break
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)