Skip to content

Commit cda6363

Browse files
committed
feat: implement caching mechanism for IP checks and add runner for concurrent execution
1 parent ca1c565 commit cda6363

8 files changed

Lines changed: 383 additions & 51 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
test:
2-
go test ./...
2+
go test -race -cover ./...
33

44
run: test
55
go run -race ./checkip.go 91.228.166.47

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,16 @@ You can also use environment variables with the same names.
9999

100100
Data used by some checks is downloaded (cached) to `$HOME/.checkip/` folder. Is gets periodically re-downloaded so it is fresh.
101101

102+
Repeated checks of the same IP address within a single `checkip` process are memoized (cached) in memory, so duplicate inputs in one run reuse already computed results instead of hitting the same providers again.
103+
102104
## Development
103105

104106
Checkip is easy to extend. If you want to add a new way of checking IP addresses:
105107

106108
1. Write a function of type [check.Func](https://pkg.go.dev/github.com/jreisinger/checkip/check#Func).
107-
2. Add it to [check.Funcs](https://pkg.go.dev/github.com/jreisinger/checkip/check#Funcs) variable.
109+
2. Add a [check.Definition](https://pkg.go.dev/github.com/jreisinger/checkip/check#Definition) to [check.Definitions](https://pkg.go.dev/github.com/jreisinger/checkip/check#Definitions).
110+
111+
New checks use process-lifetime memoization by default. If a check must always run live, set its cache policy to `check.CacheNone`.
108112

109113
Typical workflow:
110114

check/check.go

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,59 @@ const (
1515
InfoAndIsMalicious // both of the above
1616
)
1717

18-
// Funcs contains all available functions for checking IP addresses.
19-
var Funcs = []Func{
20-
AbuseIPDB,
21-
BlockList,
22-
CinsScore,
23-
DBip,
24-
DnsMX,
25-
DnsName,
26-
Firehol,
27-
IPSum,
28-
IPtoASN,
29-
IsOnAWS,
30-
MaxMind,
31-
OTX,
32-
Ping,
33-
SansISC,
34-
Shodan,
35-
Spur,
36-
Tls,
37-
UrlScan,
38-
VirusTotal,
18+
// CachePolicy says whether a check result can be reused within one process.
19+
type CachePolicy int
20+
21+
const (
22+
// CacheProcess reuses the check result for repeated IP addresses within a
23+
// single process.
24+
CacheProcess CachePolicy = iota
25+
// CacheNone always runs the check live.
26+
CacheNone
27+
)
28+
29+
// Definition describes a check and how it should be executed.
30+
type Definition struct {
31+
// Name should be unique across all registered checks.
32+
Name string
33+
Run Func
34+
// Cache defaults to CacheProcess.
35+
Cache CachePolicy
36+
}
37+
38+
// Definitions contains all available checks and their execution policy.
39+
var Definitions = []Definition{
40+
{Name: "abuseipdb.com", Run: AbuseIPDB},
41+
{Name: "blocklist.de", Run: BlockList},
42+
{Name: "cinsscore.com", Run: CinsScore},
43+
{Name: "db-ip.com", Run: DBip},
44+
{Name: "dns MX", Run: DnsMX},
45+
{Name: "dns name", Run: DnsName},
46+
{Name: "firehol.org", Run: Firehol},
47+
{Name: "ipsum.app", Run: IPSum},
48+
{Name: "iptoasn.com", Run: IPtoASN},
49+
{Name: "is on AWS", Run: IsOnAWS},
50+
{Name: "maxmind.com", Run: MaxMind},
51+
{Name: "otx.alienvault.com", Run: OTX},
52+
{Name: "ping", Run: Ping},
53+
{Name: "isc.sans.edu", Run: SansISC},
54+
{Name: "shodan.io", Run: Shodan},
55+
{Name: "spur.us", Run: Spur},
56+
{Name: "tls", Run: Tls},
57+
{Name: "urlscan.io", Run: UrlScan},
58+
{Name: "virustotal.com", Run: VirusTotal},
59+
}
60+
61+
// Funcs contains all available check functions, derived from Definitions for
62+
// backward compatibility.
63+
var Funcs = funcs(Definitions)
64+
65+
func funcs(definitions []Definition) []Func {
66+
funcs := make([]Func, 0, len(definitions))
67+
for _, definition := range definitions {
68+
funcs = append(funcs, definition.Run)
69+
}
70+
return funcs
3971
}
4072

4173
// Type is the type of a Check.

check/check_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ func TestNonEmpty(t *testing.T) {
3838
}
3939
}
4040

41+
func TestDefinitionsHaveUniqueNames(t *testing.T) {
42+
seen := make(map[string]struct{}, len(Definitions))
43+
44+
for _, definition := range Definitions {
45+
if definition.Name == "" {
46+
t.Fatal("definition name must not be empty")
47+
}
48+
if _, ok := seen[definition.Name]; ok {
49+
t.Fatalf("duplicate definition name %q", definition.Name)
50+
}
51+
seen[definition.Name] = struct{}{}
52+
}
53+
}
54+
4155
// equal tells whether a and b contain the same elements. A nil argument is
4256
// equivalent to an empty slice.
4357
func equal(a, b []string) bool {

checkip.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ func main() {
4040
log.Fatal(err)
4141
}
4242

43+
runner := cli.NewRunner(check.Definitions)
44+
4345
ipaddrs := make(chan net.IP)
4446
results := make(chan Result)
4547

@@ -55,7 +57,7 @@ func main() {
5557
wg.Add(1)
5658
go func() {
5759
for ipaddr := range ipaddrs {
58-
checks, errors := cli.Run(check.Funcs, ipaddr)
60+
checks, errors := runner.Run(ipaddr)
5961
for _, e := range errors {
6062
log.Print(e)
6163
}

cli/cli.go

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,25 @@ import (
1010
"net"
1111
"os"
1212
"sort"
13-
"sync"
1413

1514
"github.com/jreisinger/checkip/check"
1615
)
1716

18-
// Run runs checks concurrently against the ippaddr.
17+
// Run runs checks against ipaddr in the supplied order.
1918
func Run(checkFuncs []check.Func, ipaddr net.IP) (Checks, []error) {
20-
var checksMu struct {
21-
sync.Mutex
22-
checks []check.Check
23-
}
24-
var errorsMu struct {
25-
sync.Mutex
26-
errors []error
27-
}
19+
checks := make(Checks, 0, len(checkFuncs))
20+
var errors []error
2821

29-
var wg sync.WaitGroup
3022
for _, cf := range checkFuncs {
31-
wg.Add(1)
32-
go func(cf check.Func) {
33-
defer wg.Done()
34-
c, err := cf(ipaddr)
35-
if err != nil {
36-
errorsMu.Lock()
37-
errorsMu.errors = append(errorsMu.errors, err)
38-
errorsMu.Unlock()
39-
return
40-
}
41-
checksMu.Lock()
42-
checksMu.checks = append(checksMu.checks, c)
43-
checksMu.Unlock()
44-
}(cf)
23+
c, err := cf(ipaddr)
24+
if err != nil {
25+
errors = append(errors, err)
26+
continue
27+
}
28+
checks = append(checks, c)
4529
}
46-
wg.Wait()
4730

48-
return checksMu.checks, errorsMu.errors
31+
return checks, errors
4932
}
5033

5134
type Checks []check.Check

cli/runner.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package cli
2+
3+
import (
4+
"net"
5+
"sync"
6+
7+
"github.com/jreisinger/checkip/check"
8+
)
9+
10+
// Runner executes checks and memoizes cacheable results by IP address. It is
11+
// safe for concurrent use.
12+
type Runner struct {
13+
cachedDefinitions []check.Definition
14+
liveDefinitions []check.Definition
15+
16+
mu sync.Mutex
17+
memo map[string]*memoEntry
18+
}
19+
20+
type memoEntry struct {
21+
ready chan struct{}
22+
checks Checks
23+
errors []error
24+
}
25+
26+
// NewRunner creates a runner from check definitions.
27+
func NewRunner(definitions []check.Definition) *Runner {
28+
runner := &Runner{
29+
memo: make(map[string]*memoEntry),
30+
}
31+
32+
for _, definition := range definitions {
33+
if definition.Cache == check.CacheNone {
34+
runner.liveDefinitions = append(runner.liveDefinitions, definition)
35+
continue
36+
}
37+
runner.cachedDefinitions = append(runner.cachedDefinitions, definition)
38+
}
39+
40+
return runner
41+
}
42+
43+
// Run executes the configured checks for ipaddr.
44+
func (r *Runner) Run(ipaddr net.IP) (Checks, []error) {
45+
checks, errors := r.cachedRun(ipaddr)
46+
47+
liveChecks, liveErrors := runDefinitions(r.liveDefinitions, ipaddr)
48+
checks = append(checks, liveChecks...)
49+
errors = append(errors, liveErrors...)
50+
51+
return checks, errors
52+
}
53+
54+
func (r *Runner) cachedRun(ipaddr net.IP) (Checks, []error) {
55+
if len(r.cachedDefinitions) == 0 {
56+
return nil, nil
57+
}
58+
59+
key := ipaddr.String()
60+
61+
r.mu.Lock()
62+
if entry, ok := r.memo[key]; ok {
63+
r.mu.Unlock()
64+
<-entry.ready
65+
return cloneChecks(entry.checks), cloneErrors(entry.errors)
66+
}
67+
68+
entry := &memoEntry{ready: make(chan struct{})}
69+
r.memo[key] = entry
70+
r.mu.Unlock()
71+
defer close(entry.ready)
72+
73+
checks, errors := runDefinitions(r.cachedDefinitions, ipaddr)
74+
entry.checks = cloneChecks(checks)
75+
entry.errors = cloneErrors(errors)
76+
77+
return cloneChecks(checks), cloneErrors(errors)
78+
}
79+
80+
func runDefinitions(definitions []check.Definition, ipaddr net.IP) (Checks, []error) {
81+
checks := make(Checks, 0, len(definitions))
82+
var errors []error
83+
84+
for _, definition := range definitions {
85+
result, err := definition.Run(ipaddr)
86+
if err != nil {
87+
errors = append(errors, err)
88+
continue
89+
}
90+
if result.Description == "" {
91+
result.Description = definition.Name
92+
}
93+
checks = append(checks, result)
94+
}
95+
96+
return checks, errors
97+
}
98+
99+
func cloneChecks(checks Checks) Checks {
100+
if len(checks) == 0 {
101+
return nil
102+
}
103+
104+
cloned := make(Checks, len(checks))
105+
copy(cloned, checks)
106+
return cloned
107+
}
108+
109+
func cloneErrors(errors []error) []error {
110+
if len(errors) == 0 {
111+
return nil
112+
}
113+
114+
cloned := make([]error, len(errors))
115+
copy(cloned, errors)
116+
return cloned
117+
}

0 commit comments

Comments
 (0)