Merge pull request #492 from Alice39s/feature/erlang-ipv6
feat(binding/erlang): implement IPv6 dual-stack support for Erlang binding
This commit is contained in:
commit
c4eb1a14ce
|
|
@ -89,3 +89,6 @@ target
|
||||||
#vscode
|
#vscode
|
||||||
.vscode
|
.vscode
|
||||||
build
|
build
|
||||||
|
|
||||||
|
# git worktrees
|
||||||
|
.worktrees/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
.PHONY: all compile test clean bench-v4 bench-v6
|
||||||
|
|
||||||
|
all: compile
|
||||||
|
|
||||||
|
compile:
|
||||||
|
rebar3 compile
|
||||||
|
|
||||||
|
test:
|
||||||
|
rebar3 eunit
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rebar3 clean
|
||||||
|
|
||||||
|
bench-v4:
|
||||||
|
sh benchmarks/xdb-benchmark.sh ipv4
|
||||||
|
|
||||||
|
bench-v6:
|
||||||
|
sh benchmarks/xdb-benchmark.sh ipv6
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
# ip2region erlang query client
|
# ip2region Erlang query client
|
||||||
|
|
||||||
### Introduction
|
### Introduction
|
||||||
|
|
||||||
This binding implements the xdb query client in Erlang, based on the Erlang OTP Application. The query logic is implemented by the `ip2region_worker` worker process, supporting multiple worker processes for load balancing.
|
This binding implements the xdb query client in `Erlang`, based on the `Erlang OTP Application`. The query logic is implemented by the `ip2region_worker` worker process, supporting multiple worker processes for load balancing.
|
||||||
|
|
||||||
### Application Configuration
|
### Application Configuration
|
||||||
|
|
||||||
|
|
@ -15,13 +15,35 @@ The configurable parameters for this application are in `ip2region.app.src`, as
|
||||||
{poolargs, [
|
{poolargs, [
|
||||||
{size, 1}, %% Default number of worker processes
|
{size, 1}, %% Default number of worker processes
|
||||||
{max_overflow, 5} %% Maximum number of worker processes
|
{max_overflow, 5} %% Maximum number of worker processes
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"} %% Default IPv4 xdb file
|
||||||
]}
|
]}
|
||||||
]}
|
]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Dual-stack configuration (IPv4 + IPv6)
|
||||||
|
|
||||||
|
To enable IPv6 queries, add the `ipv6` entry to the `db` list and place both xdb files under `priv/`:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{env,[
|
||||||
|
{poolargs, [
|
||||||
|
{size, 1},
|
||||||
|
{max_overflow, 5}
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"},
|
||||||
|
{ipv6, "ip2region_v6.xdb"}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `xdb:search/1` interface automatically detects IPv4 and IPv6 inputs and routes them to the correct worker pool.
|
||||||
|
|
||||||
### Compile
|
### Compile
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ rebar3 compile
|
$ rebar3 compile
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -29,113 +51,130 @@ $ rebar3 compile
|
||||||
|
|
||||||
Place the xdb file in the `priv` directory, then start the Erlang node:
|
Place the xdb file in the `priv` directory, then start the Erlang node:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ rebar3 shell
|
$ rebar3 shell
|
||||||
```
|
```
|
||||||
|
|
||||||
Call the `xdb:search/1` interface in the Erlang shell to query IP address information. This interface supports IP addresses represented as list strings, binary strings, tuples, and integers, as follows:
|
Call the `xdb:search/1` interface in the Erlang shell to query IP address information. This interface supports IP addresses represented as list strings, binary strings, tuples, and integers:
|
||||||
|
|
||||||
```
|
```erlang
|
||||||
1> xdb:search("1.0.8.0").
|
1> xdb:search("1.0.8.0").
|
||||||
[20013,22269,124,48,124,24191,19996,30465,124,24191,24030,
|
[20013,22269,124,24191,19996,30465,124,24191,24030,24066,
|
||||||
24066,124,30005,20449]
|
124,20013,22269,30005,20449,124,67,78]
|
||||||
2>
|
2>
|
||||||
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
4> io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
4> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
5> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
6> io:format("~ts~n", [xdb:search(16779264)]).
|
6> io:format("~ts~n", [xdb:search(16779264)]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
|
```
|
||||||
|
|
||||||
|
With dual-stack enabled, IPv6 addresses are supported in the same way:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
3> io:format("~ts~n", [xdb:search({8193,18528,18528,0,0,0,0,34952})]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
* Add the dependency in `rebar.config`
|
- Add the dependency in `rebar.config`
|
||||||
|
|
||||||
```
|
```erlang
|
||||||
{deps, [
|
{deps, [
|
||||||
ip2region
|
ip2region
|
||||||
]}.
|
]}.
|
||||||
```
|
```
|
||||||
|
|
||||||
* Start the ip2region Application
|
- Start the ip2region Application
|
||||||
|
|
||||||
```
|
```erlang
|
||||||
......
|
{ok, _} = application:ensure_all_started(ip2region).
|
||||||
|
|
||||||
application:ensure_started(ip2region),
|
|
||||||
|
|
||||||
......
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Call the `xdb:search/1` interface to query IP information
|
- Call the `xdb:search/1` interface to query IP information
|
||||||
|
|
||||||
```
|
```erlang
|
||||||
......
|
xdb:search("1.0.8.0").
|
||||||
|
|
||||||
ip2region:search("1.0.8.0"),
|
|
||||||
|
|
||||||
......
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Unit Test
|
### Unit Test
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ rebar3 eunit
|
$ rebar3 eunit
|
||||||
===> Verifying dependencies...
|
===> Verifying dependencies...
|
||||||
===> Analyzing applications...
|
===> Analyzing applications...
|
||||||
===> Compiling ip2region
|
===> Compiling ip2region
|
||||||
===> Performing EUnit tests...
|
===> Performing EUnit tests...
|
||||||
=INFO REPORT==== 17-Jan-2023::11:52:59.920155 ===
|
=INFO REPORT==== 28-Jun-2026::04:53:28 ===
|
||||||
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
||||||
|
|
||||||
....
|
....
|
||||||
Finished in 0.074 seconds
|
Finished in 0.192 seconds
|
||||||
4 tests, 0 failures
|
63 tests, 0 failures
|
||||||
```
|
```
|
||||||
|
|
||||||
### Benchmark
|
### Benchmark
|
||||||
|
|
||||||
```
|
Both IPv4 and IPv6 benchmarks share the same script. Run it with the desired IP version:
|
||||||
|
|
||||||
|
> `cold` = first pass over the source file: each IP triggers a real search and the
|
||||||
|
> result is written into the ETS cache. `warm` = second pass over the same list,
|
||||||
|
> where every lookup is served directly from the ETS cache.
|
||||||
|
|
||||||
|
```bash
|
||||||
$ cd benchmarks/
|
$ cd benchmarks/
|
||||||
$ sh xdb-benchmark.sh
|
$ sh xdb-benchmark.sh ipv4
|
||||||
===> Verifying dependencies...
|
```
|
||||||
===> Analyzing applications...
|
|
||||||
===> Compiling ip2region
|
For IPv6:
|
||||||
Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
|
|
||||||
|
```bash
|
||||||
Eshell V12.3.2.2 (abort with ^G)
|
$ sh xdb-benchmark.sh ipv6
|
||||||
1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 ===
|
```
|
||||||
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb
|
|
||||||
|
Or use the Makefile targets from the `binding/erlang` directory:
|
||||||
===> Booted ip2region
|
|
||||||
===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()."
|
```bash
|
||||||
CPU info:
|
$ make bench-v4
|
||||||
model name : AMD EPYC 7K62 48-Core Processor
|
$ make bench-v6
|
||||||
cache size : 512 KB
|
```
|
||||||
cpu MHz : 2595.124
|
|
||||||
bogomips : 5190.24
|
#### IPv4 benchmark example
|
||||||
cores/threads : 2
|
|
||||||
|
```bash
|
||||||
Erlang info:
|
System:
|
||||||
system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
|
CPU : Apple M4
|
||||||
load test data use 4.835593s
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
start run benchmark tests
|
Loaded : 487169 IPs in 1.335 s
|
||||||
|
|
||||||
search from file:
|
Benchmarks:
|
||||||
ip count:683844,
|
cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op)
|
||||||
total time: 28.201699s,
|
warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op)
|
||||||
search 24248.326315375536 times per second,
|
|
||||||
use 41.23995969841075 micro second per search
|
Done.
|
||||||
|
```
|
||||||
search from cache:
|
|
||||||
ip count:683844,
|
#### IPv6 benchmark example
|
||||||
total time: 0.671801s,
|
|
||||||
search 1017926.4395259906 times per second,
|
```bash
|
||||||
use 0.9823892583688677 micro second per search
|
System:
|
||||||
|
CPU : Apple M4
|
||||||
benchmark test finish
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
|
Loaded : 638953 IPs in 2.949 s
|
||||||
|
|
||||||
|
Benchmarks:
|
||||||
|
cold total= 20.504s count= 638953 qps= 31162.52 avg= 0.032090 ms/op (32.090 us/op)
|
||||||
|
warm total= 0.444s count= 638953 qps= 1437781.56 avg= 0.000696 ms/op ( 0.696 us/op)
|
||||||
|
|
||||||
|
Done.
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,179 @@
|
||||||
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
:globe_with_meridians: [中文简体](README_zh.md) | [English](README.md)
|
||||||
|
|
||||||
# ip2region erlang 查询客户端
|
# ip2region Erlang 查询客户端
|
||||||
|
|
||||||
### 简介
|
### 简介
|
||||||
该bingding以erlang语言实现xdb查询客户端,基于Erlang OTP Application,查询逻辑由ip2region_worker工作进程实现,支持配多个工作进程来进行负载均衡。
|
|
||||||
|
该 bingding 以 `Erlang` 语言实现 xdb 查询客户端,基于 `Erlang OTP Application`,查询逻辑由 `ip2region_worker` 工作进程实现,支持配多个工作进程来进行负载均衡。
|
||||||
|
|
||||||
### 应用配置
|
### 应用配置
|
||||||
该应用可配置的参数在ip2region.app.src中,如下:
|
|
||||||
``` erlang
|
该应用可配置的参数在 `ip2region.app.src` 中, 如下:
|
||||||
|
|
||||||
|
```erlang
|
||||||
{env,[
|
{env,[
|
||||||
{poolargs, [
|
{poolargs, [
|
||||||
{size, 1}, %% 工作进程默认数量
|
{size, 1}, %% 工作进程默认数量
|
||||||
{max_overflow, 5} %% 工作进程最大数量
|
{max_overflow, 5} %% 工作进程最大数量
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"} %% 默认 IPv4 xdb 文件
|
||||||
]}
|
]}
|
||||||
]}
|
]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 双栈配置(IPv4 + IPv6)
|
||||||
|
|
||||||
|
如需启用 IPv6 查询,在 `db` 列表中加入 `ipv6` 项,并将两个 xdb 文件放到 `priv/` 目录下:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
{env,[
|
||||||
|
{poolargs, [
|
||||||
|
{size, 1},
|
||||||
|
{max_overflow, 5}
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"},
|
||||||
|
{ipv6, "ip2region_v6.xdb"}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
`xdb:search/1` 接口会自动识别 IPv4 与 IPv6 输入,并将其路由到对应的工作进程池。
|
||||||
|
|
||||||
### 编译
|
### 编译
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ rebar3 compile
|
$ rebar3 compile
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行
|
### 运行
|
||||||
将xdb文件放到priv目录下,然后启动erlang节点:
|
|
||||||
```
|
将 xdb 文件放到 `priv` 目录下,然后启动 Erlang 节点:
|
||||||
|
|
||||||
|
```bash
|
||||||
$ rebar3 shell
|
$ rebar3 shell
|
||||||
```
|
```
|
||||||
在erlang shell中调用xdb:search/1接口查询Ip地址信息, 该接口支持以list格式字符串、binary格式字符串、tuple和整数表示的IP地址,如下:
|
|
||||||
```
|
在 Erlang shell 中调用 `xdb:search/1` 接口查询 IP 地址信息。该接口支持 list 字符串、binary 字符串、tuple 和整数表示的 IP 地址:
|
||||||
|
|
||||||
|
```erlang
|
||||||
1> xdb:search("1.0.8.0").
|
1> xdb:search("1.0.8.0").
|
||||||
[20013,22269,124,48,124,24191,19996,30465,124,24191,24030,
|
[20013,22269,124,24191,19996,30465,124,24191,24030,24066,
|
||||||
24066,124,30005,20449]
|
124,20013,22269,30005,20449,124,67,78]
|
||||||
2>
|
2>
|
||||||
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
3> io:format("~ts~n", [xdb:search("1.0.8.0")]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
4> io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
4> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
5> io:format("~ts~n", [xdb:search({1,0,8,0})]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
6> io:format("~ts~n", [xdb:search(16779264)]).
|
6> io:format("~ts~n", [xdb:search(16779264)]).
|
||||||
中国|0|广东省|广州市|电信
|
中国|广东省|广州市|中国电信|CN
|
||||||
|
```
|
||||||
|
|
||||||
|
启用双栈后,IPv6 地址也按同样方式支持:
|
||||||
|
|
||||||
|
```erlang
|
||||||
|
1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
|
3> io:format("~ts~n", [xdb:search({8193,18528,18528,0,0,0,0,34952})]).
|
||||||
|
United States|Florida|Miami|Google LLC|US
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用方法
|
### 使用方法
|
||||||
* 在rebar.config中引入依赖
|
|
||||||
```
|
- 在 `rebar.config` 中引入依赖
|
||||||
|
|
||||||
|
```erlang
|
||||||
{deps, [
|
{deps, [
|
||||||
ip2region
|
ip2region
|
||||||
]}.
|
]}.
|
||||||
```
|
```
|
||||||
* 启动ip2region Application
|
|
||||||
```
|
|
||||||
......
|
|
||||||
|
|
||||||
application:ensure_started(ip2region),
|
- 启动 ip2region Application
|
||||||
|
|
||||||
......
|
```erlang
|
||||||
|
{ok, _} = application:ensure_all_started(ip2region).
|
||||||
```
|
```
|
||||||
|
|
||||||
* 调用xdb:search/1接口查询IP信息
|
- 调用 `xdb:search/1` 接口查询 IP 信息
|
||||||
```
|
|
||||||
......
|
|
||||||
|
|
||||||
ip2region:search("1.0.8.0"),
|
```erlang
|
||||||
|
xdb:search("1.0.8.0").
|
||||||
......
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 单元测试
|
### 单元测试
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ rebar3 eunit
|
$ rebar3 eunit
|
||||||
===> Verifying dependencies...
|
===> Verifying dependencies...
|
||||||
===> Analyzing applications...
|
===> Analyzing applications...
|
||||||
===> Compiling ip2region
|
===> Compiling ip2region
|
||||||
===> Performing EUnit tests...
|
===> Performing EUnit tests...
|
||||||
=INFO REPORT==== 17-Jan-2023::11:52:59.920155 ===
|
=INFO REPORT==== 28-Jun-2026::04:53:28 ===
|
||||||
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb
|
||||||
|
|
||||||
....
|
....
|
||||||
Finished in 0.074 seconds
|
Finished in 0.192 seconds
|
||||||
4 tests, 0 failures
|
63 tests, 0 failures
|
||||||
```
|
```
|
||||||
|
|
||||||
### 基准测试
|
### 基准测试
|
||||||
```
|
|
||||||
|
IPv4 与 IPv6 共用一个脚本,通过参数指定版本:
|
||||||
|
|
||||||
|
> `cold` = 第一次遍历源文件:每个 IP 都会触发真实查询,并把结果写入 ETS 缓存。
|
||||||
|
> `warm` = 第二次遍历同一列表,所有查询都直接命中 ETS 缓存。
|
||||||
|
|
||||||
|
```bash
|
||||||
$ cd benchmarks/
|
$ cd benchmarks/
|
||||||
$ sh xdb-benchmark.sh
|
$ sh xdb-benchmark.sh ipv4
|
||||||
===> Verifying dependencies...
|
```
|
||||||
===> Analyzing applications...
|
|
||||||
===> Compiling ip2region
|
IPv6:
|
||||||
Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
|
|
||||||
|
```bash
|
||||||
Eshell V12.3.2.2 (abort with ^G)
|
$ sh xdb-benchmark.sh ipv6
|
||||||
1> =INFO REPORT==== 17-Jan-2023::11:37:35.631095 ===
|
```
|
||||||
XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/default/lib/ip2region/priv/ip2region.xdb
|
|
||||||
|
也可以在 `binding/erlang` 目录直接用 Makefile:
|
||||||
===> Booted ip2region
|
|
||||||
===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()."
|
```bash
|
||||||
CPU info:
|
$ make bench-v4
|
||||||
model name : AMD EPYC 7K62 48-Core Processor
|
$ make bench-v6
|
||||||
cache size : 512 KB
|
```
|
||||||
cpu MHz : 2595.124
|
|
||||||
bogomips : 5190.24
|
#### IPv4 基准测试示例
|
||||||
cores/threads : 2
|
|
||||||
|
```bash
|
||||||
Erlang info:
|
System:
|
||||||
system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
|
CPU : Apple M4
|
||||||
load test data use 4.835593s
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
start run benchmark tests
|
Loaded : 487169 IPs in 1.335 s
|
||||||
|
|
||||||
search from file:
|
Benchmarks:
|
||||||
ip count:683844,
|
cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op)
|
||||||
total time: 28.201699s,
|
warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op)
|
||||||
search 24248.326315375536 times per second,
|
|
||||||
use 41.23995969841075 micro second per search
|
Done.
|
||||||
|
```
|
||||||
search from cache:
|
|
||||||
ip count:683844,
|
#### IPv6 基准测试示例
|
||||||
total time: 0.671801s,
|
|
||||||
search 1017926.4395259906 times per second,
|
```bash
|
||||||
use 0.9823892583688677 micro second per search
|
System:
|
||||||
|
CPU : Apple M4
|
||||||
benchmark test finish
|
Cores : 10 cores / 10 threads
|
||||||
|
Erlang : Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace]
|
||||||
|
Loaded : 638953 IPs in 2.949 s
|
||||||
|
|
||||||
|
Benchmarks:
|
||||||
|
cold total= 20.504s count= 638953 qps= 31162.52 avg= 0.032090 ms/op (32.090 us/op)
|
||||||
|
warm total= 0.444s count= 638953 qps= 1437781.56 avg= 0.000696 ms/op ( 0.696 us/op)
|
||||||
|
|
||||||
|
Done.
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,37 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
cd ..
|
# Run from binding/erlang so paths and rebar3 artifacts are correct.
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
rebar3 shell --eval="xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()."
|
VERSION="${1:-ipv4}"
|
||||||
|
PROJECT_ROOT="$(cd ../.. && pwd)"
|
||||||
|
|
||||||
|
if [ "$VERSION" = "ipv6" ]; then
|
||||||
|
DB_FILE="${PROJECT_ROOT}/data/ip2region_v6.xdb"
|
||||||
|
DATA_FILE="${PROJECT_ROOT}/data/ipv6_source.txt"
|
||||||
|
DB_CONFIG="[{ipv6, \"${DB_FILE}\"}]"
|
||||||
|
elif [ "$VERSION" = "ipv4" ]; then
|
||||||
|
DATA_FILE="${PROJECT_ROOT}/data/ipv4_source.txt"
|
||||||
|
DB_CONFIG="[{ipv4, \"ip2region.xdb\"}]"
|
||||||
|
else
|
||||||
|
echo "Usage: $0 [ipv4|ipv6]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rebar3 compile
|
||||||
|
|
||||||
|
# shellcheck disable=SC2089
|
||||||
|
EVAL="application:load(ip2region), \
|
||||||
|
application:set_env(ip2region, db, ${DB_CONFIG}), \
|
||||||
|
application:set_env(ip2region, poolargs, [{size,4},{max_overflow,8}]), \
|
||||||
|
{ok,_}=application:ensure_all_started(ip2region), \
|
||||||
|
xdb_benchmark:main(\"${DATA_FILE}\"), \
|
||||||
|
init:stop()."
|
||||||
|
|
||||||
|
# shellcheck disable=SC2090
|
||||||
|
erl -pa _build/default/lib/poolboy/ebin \
|
||||||
|
-pa _build/default/lib/ip2region/ebin \
|
||||||
|
-kernel logger_level error \
|
||||||
|
-noshell \
|
||||||
|
-eval "${EVAL}"
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,39 @@
|
||||||
-define(NONE, none).
|
-define(NONE, none).
|
||||||
-define(APP_NAME, ip2region).
|
-define(APP_NAME, ip2region).
|
||||||
|
|
||||||
-define(XDB_VECTOR_INDEX, ets_xdb_vector_index).
|
|
||||||
-define(XDB_SEGMENT_INDEX, ets_xdb_segement_index).
|
|
||||||
-define(IP2REGION_CACHE, ets_ip2region_cache).
|
|
||||||
|
|
||||||
|
|
||||||
-define(XDB_HEADER_SIZE, 256).
|
-define(XDB_HEADER_SIZE, 256).
|
||||||
-define(XDB_VECTOR_COLS, 256).
|
-define(XDB_VECTOR_COLS, 256).
|
||||||
-define(XDB_VECTOR_INDEX_SIZE, 8).
|
-define(XDB_VECTOR_INDEX_SIZE, 8).
|
||||||
-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256
|
-define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256
|
||||||
|
|
||||||
-define(XDB_SEGMENT_INDEX_SIZE, 14).
|
%% IP versions as stored in xdb header
|
||||||
|
-define(IP_VERSION_4, 4).
|
||||||
|
-define(IP_VERSION_6, 6).
|
||||||
|
|
||||||
-define(IP2REGION_POOL, ip2region_pool).
|
%% Segment index sizes (bytes): start_ip + end_ip + data_len + data_ptr
|
||||||
|
-define(XDB_SEGMENT_INDEX_SIZE_V4, 14). %% 4 + 4 + 2 + 4
|
||||||
|
-define(XDB_SEGMENT_INDEX_SIZE_V6, 38). %% 16 + 16 + 2 + 4
|
||||||
|
|
||||||
|
%% Per-version ETS tables
|
||||||
|
-define(XDB_VECTOR_INDEX_V4, ets_xdb_vector_index_v4).
|
||||||
|
-define(XDB_VECTOR_INDEX_V6, ets_xdb_vector_index_v6).
|
||||||
|
-define(XDB_SEGMENT_INDEX_V4, ets_xdb_segment_index_v4).
|
||||||
|
-define(XDB_SEGMENT_INDEX_V6, ets_xdb_segment_index_v6).
|
||||||
|
-define(IP2REGION_CACHE_V4, ets_ip2region_cache_v4).
|
||||||
|
-define(IP2REGION_CACHE_V6, ets_ip2region_cache_v6).
|
||||||
|
|
||||||
|
%% Per-version pool names.
|
||||||
|
-define(IP2REGION_POOL_V4, ip2region_pool_v4).
|
||||||
|
-define(IP2REGION_POOL_V6, ip2region_pool_v6).
|
||||||
|
|
||||||
|
-record(xdb_header, {
|
||||||
|
version :: non_neg_integer(),
|
||||||
|
index_policy :: non_neg_integer(),
|
||||||
|
created_at :: non_neg_integer(),
|
||||||
|
start_index_ptr :: non_neg_integer(),
|
||||||
|
end_index_ptr :: non_neg_integer(),
|
||||||
|
ip_version :: non_neg_integer(),
|
||||||
|
runtime_ptr_bytes :: non_neg_integer()
|
||||||
|
}).
|
||||||
|
|
||||||
-ifndef(IF).
|
|
||||||
-define(IF(C, T, F), case (C) of true -> (T); false -> (F) end).
|
|
||||||
-define(IF(C, T), ?IF(C, T, skip)).
|
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-endif.
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{erl_opts, [
|
{erl_opts, [
|
||||||
debug_info,
|
debug_info,
|
||||||
export_all,
|
export_all,
|
||||||
nowarn_export_all
|
nowarn_export_all
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
{ex_doc, [
|
{ex_doc, [
|
||||||
{extras, ["README.md"]},
|
{extras, ["README.md"]},
|
||||||
{main, "README.md"},
|
{main, "README.md"},
|
||||||
{source_url, "https://github.com/leihua996/ip2region/tree/master/binding/erlang"}
|
{source_url, "https://github.com/lionsoul2014/ip2region/tree/master/binding/erlang"}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{hex, [{doc, ex_doc}]}.
|
{hex, [{doc, ex_doc}]}.
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,20 @@
|
||||||
{mod, {ip2region_app, []}},
|
{mod, {ip2region_app, []}},
|
||||||
{applications,
|
{applications,
|
||||||
[kernel,
|
[kernel,
|
||||||
stdlib
|
stdlib,
|
||||||
|
poolboy
|
||||||
]},
|
]},
|
||||||
{env,[
|
{env,[
|
||||||
{poolargs, [
|
{poolargs, [
|
||||||
{size, 1},
|
{size, 1},
|
||||||
{max_overflow, 5}
|
{max_overflow, 5}
|
||||||
|
]},
|
||||||
|
{db, [
|
||||||
|
{ipv4, "ip2region.xdb"}
|
||||||
]}
|
]}
|
||||||
]},
|
]},
|
||||||
{modules, []},
|
{modules, []},
|
||||||
|
|
||||||
{licenses, ["Apache-2.0"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
{links, [{"Github", "https://github.com/leihua996/ip2region/tree/master/binding/erlang"}]}
|
{links, [{"Github", "https://github.com/lionsoul2014/ip2region/tree/master/binding/erlang"}]}
|
||||||
]}.
|
]}.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
%% Use of this source code is governed by a Apache2.0-style
|
%% Use of this source code is governed by a Apache2.0-style
|
||||||
%% license that can be found in the LICENSE file.
|
%% license that can be found in the LICENSE file.
|
||||||
%%
|
%%
|
||||||
%% @doc ip2region top level supervisor.
|
%% @doc ip2region top level supervisor.
|
||||||
%% @end
|
%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
start_link() ->
|
start_link() ->
|
||||||
{ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []),
|
{ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []),
|
||||||
{ok, _PoolPid} = start_ip2region_pool(SupPid),
|
|
||||||
{ok, SupPid}.
|
{ok, SupPid}.
|
||||||
|
|
||||||
%% sup_flags() = #{strategy => strategy(), % optional
|
%% sup_flags() = #{strategy => strategy(), % optional
|
||||||
|
|
@ -35,21 +34,45 @@ init([]) ->
|
||||||
SupFlags = #{strategy => one_for_one,
|
SupFlags = #{strategy => one_for_one,
|
||||||
intensity => 10,
|
intensity => 10,
|
||||||
period => 5},
|
period => 5},
|
||||||
ChildSpecs = [],
|
ChildSpecs = pool_child_specs(),
|
||||||
{ok, {SupFlags, ChildSpecs}}.
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
||||||
%% internal functions
|
%% internal functions
|
||||||
%%
|
%%
|
||||||
create_table() ->
|
create_table() ->
|
||||||
Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],
|
Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],
|
||||||
ets:new(?XDB_VECTOR_INDEX, Opts),
|
%% Version-specific tables for dual-stack support
|
||||||
ets:new(?XDB_SEGMENT_INDEX, Opts),
|
ensure_table(?XDB_VECTOR_INDEX_V4, Opts),
|
||||||
ets:new(?IP2REGION_CACHE, Opts).
|
ensure_table(?XDB_VECTOR_INDEX_V6, Opts),
|
||||||
|
ensure_table(?XDB_SEGMENT_INDEX_V4, Opts),
|
||||||
|
ensure_table(?XDB_SEGMENT_INDEX_V6, Opts),
|
||||||
|
ensure_table(?IP2REGION_CACHE_V4, Opts),
|
||||||
|
ensure_table(?IP2REGION_CACHE_V6, Opts).
|
||||||
|
|
||||||
start_ip2region_pool(Sup) ->
|
ensure_table(Name, Opts) ->
|
||||||
|
case ets:whereis(Name) of
|
||||||
|
undefined -> ets:new(Name, Opts);
|
||||||
|
_ -> ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
pool_child_specs() ->
|
||||||
|
{ok, DbConfig} = application:get_env(db),
|
||||||
{ok, PoolArgsCfg} = application:get_env(poolargs),
|
{ok, PoolArgsCfg} = application:get_env(poolargs),
|
||||||
PoolName = ?IP2REGION_POOL,
|
lists:foldl(
|
||||||
PoolArgs = [{strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg],
|
fun({ipv4, File}, Acc) ->
|
||||||
WorkerArgs = [],
|
[make_pool_spec(?IP2REGION_POOL_V4, ipv4, File, PoolArgsCfg) | Acc];
|
||||||
ChildSpecs = poolboy:child_spec(PoolName, PoolArgs, WorkerArgs),
|
({ipv6, File}, Acc) ->
|
||||||
supervisor:start_child(Sup, ChildSpecs).
|
[make_pool_spec(?IP2REGION_POOL_V6, ipv6, File, PoolArgsCfg) | Acc];
|
||||||
|
(_, Acc) ->
|
||||||
|
Acc
|
||||||
|
end, [], DbConfig).
|
||||||
|
|
||||||
|
make_pool_spec(PoolName, Version, File, PoolArgsCfg) ->
|
||||||
|
PoolArgs = [
|
||||||
|
{strategy, fifo},
|
||||||
|
{name, {local, PoolName}},
|
||||||
|
{worker_module, ip2region_worker}
|
||||||
|
| PoolArgsCfg
|
||||||
|
],
|
||||||
|
WorkerArgs = [{xdb_file, File}, {expected_version, Version}],
|
||||||
|
poolboy:child_spec(PoolName, PoolArgs, WorkerArgs).
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,70 @@
|
||||||
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
%% Use of this source code is governed by a Apache2.0-style
|
%% Use of this source code is governed by a Apache2.0-style
|
||||||
%% license that can be found in the LICENSE file.
|
%% license that can be found in the LICENSE file.
|
||||||
%%
|
%%
|
||||||
%% @doc
|
%% @doc
|
||||||
%% ip2region utils
|
%% ip2region utils: IPv4/IPv6 parsing and version detection.
|
||||||
%% @end
|
%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ip2region_util).
|
-module(ip2region_util).
|
||||||
-export([ipv4_to_n/1]).
|
|
||||||
|
|
||||||
|
-export([ipv4_to_n/1, ip_version/1, ip_to_bytes/1]).
|
||||||
|
|
||||||
ipv4_to_n(IntIp) when is_integer(IntIp) -> IntIp;
|
-define(IS_UINT8(V), is_integer(V), V >= 0, V =< 255).
|
||||||
ipv4_to_n({A, B, C, D}) ->
|
-define(IS_UINT16(V), is_integer(V), V >= 0, V =< 65535).
|
||||||
<<N:32>> = <<A, B, C, D>>,
|
|
||||||
N;
|
-spec ip_version(Ip :: tuple() | list() | binary() | integer()) ->
|
||||||
ipv4_to_n(Ip) when is_binary(Ip) ->
|
ipv4 | ipv6 | {error, atom()}.
|
||||||
ipv4_to_n(binary_to_list(Ip));
|
ip_version(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF -> ipv4;
|
||||||
ipv4_to_n(Ip) when is_list(Ip) ->
|
ip_version({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) -> ipv4;
|
||||||
|
ip_version({A, B, C, D, E, F, G, H}) when ?IS_UINT16(A), ?IS_UINT16(B), ?IS_UINT16(C), ?IS_UINT16(D),
|
||||||
|
?IS_UINT16(E), ?IS_UINT16(F), ?IS_UINT16(G), ?IS_UINT16(H) -> ipv6;
|
||||||
|
ip_version(Ip) when is_binary(Ip) ->
|
||||||
|
ip_version(binary_to_list(Ip));
|
||||||
|
ip_version(Ip) when is_list(Ip) ->
|
||||||
case inet_parse:address(Ip) of
|
case inet_parse:address(Ip) of
|
||||||
{ok, Addr} ->
|
{ok, {_, _, _, _}} -> ipv4;
|
||||||
ipv4_to_n(Addr);
|
{ok, {_, _, _, _, _, _, _, _}} -> ipv6;
|
||||||
|
_ -> {error, bad_ip_format}
|
||||||
|
end;
|
||||||
|
ip_version(_) ->
|
||||||
|
{error, bad_ip_format}.
|
||||||
|
|
||||||
|
-spec ip_to_bytes(Ip :: tuple() | list() | binary() | integer()) ->
|
||||||
|
{ok, ipv4 | ipv6, binary()} | {error, atom()}.
|
||||||
|
ip_to_bytes(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF ->
|
||||||
|
{ok, ipv4, <<Ip:32>>};
|
||||||
|
ip_to_bytes({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) ->
|
||||||
|
{ok, ipv4, <<A, B, C, D>>};
|
||||||
|
ip_to_bytes({A, B, C, D, E, F, G, H}) when ?IS_UINT16(A), ?IS_UINT16(B), ?IS_UINT16(C), ?IS_UINT16(D),
|
||||||
|
?IS_UINT16(E), ?IS_UINT16(F), ?IS_UINT16(G), ?IS_UINT16(H) ->
|
||||||
|
{ok, ipv6, <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>};
|
||||||
|
ip_to_bytes(Ip) when is_binary(Ip) ->
|
||||||
|
ip_to_bytes(binary_to_list(Ip));
|
||||||
|
ip_to_bytes(Ip) when is_list(Ip) ->
|
||||||
|
case inet_parse:address(Ip) of
|
||||||
|
{ok, {A, B, C, D}} ->
|
||||||
|
{ok, ipv4, <<A, B, C, D>>};
|
||||||
|
{ok, {A, B, C, D, E, F, G, H}} ->
|
||||||
|
{ok, ipv6, <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>};
|
||||||
_ ->
|
_ ->
|
||||||
{error, bad_ip_format}
|
{error, bad_ip_format}
|
||||||
end.
|
end;
|
||||||
|
ip_to_bytes(_) ->
|
||||||
|
{error, bad_ip_format}.
|
||||||
|
|
||||||
|
-spec ipv4_to_n(Ip :: tuple() | list() | binary() | integer()) ->
|
||||||
|
non_neg_integer() | {error, atom()}.
|
||||||
|
ipv4_to_n(IntIp) when is_integer(IntIp), IntIp >= 0, IntIp =< 16#FFFFFFFF -> IntIp;
|
||||||
|
ipv4_to_n({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) ->
|
||||||
|
<<N:32>> = <<A, B, C, D>>,
|
||||||
|
N;
|
||||||
|
ipv4_to_n(Ip) when is_binary(Ip) ->
|
||||||
|
ipv4_to_n(binary_to_list(Ip));
|
||||||
|
ipv4_to_n(Ip) when is_list(Ip) ->
|
||||||
|
case ip_to_bytes(Ip) of
|
||||||
|
{ok, ipv4, <<N:32>>} -> N;
|
||||||
|
_ -> {error, bad_ip_format}
|
||||||
|
end;
|
||||||
|
ipv4_to_n(_) ->
|
||||||
|
{error, bad_ip_format}.
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
%% Use of this source code is governed by a Apache2.0-style
|
%% Use of this source code is governed by a Apache2.0-style
|
||||||
%% license that can be found in the LICENSE file.
|
%% license that can be found in the LICENSE file.
|
||||||
%%
|
%%
|
||||||
%% @doc
|
%% @doc
|
||||||
%% ip2region xdb client worker
|
%% ip2region xdb client worker, now version-aware (IPv4/IPv6).
|
||||||
%% @end
|
%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(ip2region_worker).
|
-module(ip2region_worker).
|
||||||
|
|
@ -15,7 +15,11 @@
|
||||||
-export([start/1, stop/1, start_link/1]).
|
-export([start/1, stop/1, start_link/1]).
|
||||||
-export([search/2]).
|
-export([search/2]).
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
-record(state, {xdb_fd}).
|
|
||||||
|
-record(state, {
|
||||||
|
xdb_fd,
|
||||||
|
version :: ipv4 | ipv6
|
||||||
|
}).
|
||||||
|
|
||||||
%%==========================================
|
%%==========================================
|
||||||
%% API
|
%% API
|
||||||
|
|
@ -28,7 +32,6 @@ start_link(Args) ->
|
||||||
Opts = [{spawn_opt, [{min_heap_size, 6000}]}],
|
Opts = [{spawn_opt, [{min_heap_size, 6000}]}],
|
||||||
gen_server:start_link(?MODULE, Args, Opts).
|
gen_server:start_link(?MODULE, Args, Opts).
|
||||||
|
|
||||||
|
|
||||||
stop(Pid) ->
|
stop(Pid) ->
|
||||||
gen_server:call(Pid, stop).
|
gen_server:call(Pid, stop).
|
||||||
|
|
||||||
|
|
@ -38,26 +41,46 @@ search(Pid, Ip) ->
|
||||||
%%==========================================
|
%%==========================================
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
%% =========================================
|
%% =========================================
|
||||||
init(_Args) ->
|
init(Args) ->
|
||||||
process_flag(trap_exit, true),
|
process_flag(trap_exit, true),
|
||||||
AppName =
|
AppName =
|
||||||
case application:get_application() of
|
case application:get_application() of
|
||||||
{ok, AName} -> AName;
|
{ok, AName} -> AName;
|
||||||
_ -> ?APP_NAME
|
_ -> ?APP_NAME
|
||||||
end,
|
end,
|
||||||
PrivDir = code:priv_dir(AppName),
|
PrivDir = code:priv_dir(AppName),
|
||||||
XdbFileName = filename:join([PrivDir, "ip2region.xdb"]),
|
XdbFileName =
|
||||||
|
case proplists:get_value(xdb_file, Args) of
|
||||||
|
undefined -> filename:join([PrivDir, "ip2region.xdb"]);
|
||||||
|
Path ->
|
||||||
|
case filename:pathtype(Path) of
|
||||||
|
absolute -> Path;
|
||||||
|
_ -> filename:join([PrivDir, Path])
|
||||||
|
end
|
||||||
|
end,
|
||||||
error_logger:info_report(io_lib:format("XdbFile:~s~n", [XdbFileName])),
|
error_logger:info_report(io_lib:format("XdbFile:~s~n", [XdbFileName])),
|
||||||
{ok, IoDevice} = file:open(XdbFileName, [read, binary]),
|
{ok, IoDevice} = file:open(XdbFileName, [read, binary]),
|
||||||
load_vector_index(IoDevice),
|
{ok, HeaderBin} = file:read(IoDevice, ?XDB_HEADER_SIZE),
|
||||||
{ok, #state{xdb_fd = IoDevice}}.
|
{ok, Header} = ip2region_xdb:parse_header(HeaderBin),
|
||||||
|
ExpectedVersion = proplists:get_value(expected_version, Args),
|
||||||
|
case resolve_version(Header) of
|
||||||
|
{ok, Version} when ExpectedVersion =:= undefined; ExpectedVersion =:= Version ->
|
||||||
|
load_vector_index(IoDevice, Version),
|
||||||
|
{ok, #state{xdb_fd = IoDevice, version = Version}};
|
||||||
|
{ok, Version} ->
|
||||||
|
file:close(IoDevice),
|
||||||
|
{stop, {xdb_version_mismatch, ExpectedVersion, Version}};
|
||||||
|
{error, Reason} ->
|
||||||
|
file:close(IoDevice),
|
||||||
|
{stop, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
handle_call(Request, From, State) ->
|
handle_call(Request, From, State) ->
|
||||||
try
|
try
|
||||||
do_call(Request, From, State)
|
do_call(Request, From, State)
|
||||||
catch
|
catch
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
error_logger:error_report(io_lib:format("~p handle call error, Req:~p ~p, stacktrace:~p~n",
|
error_logger:error_report(io_lib:format("~p handle call error, Req:~p ~p, stacktrace:~p~n",
|
||||||
[?MODULE, Request, {Class, Error}, Stacktrace])),
|
[?MODULE, Request, {Class, Error}, Stacktrace])),
|
||||||
{reply, {error, {Class, Error}}, State}
|
{reply, {error, {Class, Error}}, State}
|
||||||
end.
|
end.
|
||||||
|
|
@ -67,7 +90,7 @@ handle_cast(Msg, State) ->
|
||||||
do_cast(Msg, State)
|
do_cast(Msg, State)
|
||||||
catch
|
catch
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
error_logger:error_report(io_lib:format("~p handle cast error, Msg:~p, ~p, stacktrace:~w~n",
|
error_logger:error_report(io_lib:format("~p handle cast error, Msg:~p, ~p, stacktrace:~w~n",
|
||||||
[?MODULE, Msg, {Class, Error}, Stacktrace])),
|
[?MODULE, Msg, {Class, Error}, Stacktrace])),
|
||||||
{noreply, State}
|
{noreply, State}
|
||||||
end.
|
end.
|
||||||
|
|
@ -77,7 +100,7 @@ handle_info(Info, State) ->
|
||||||
do_info(Info, State)
|
do_info(Info, State)
|
||||||
catch
|
catch
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
error_logger:error_report(io_lib:format("~p handle info error, Info:~p, ~p, stacktrace:~p~n",
|
error_logger:error_report(io_lib:format("~p handle info error, Info:~p, ~p, stacktrace:~p~n",
|
||||||
[?MODULE, Info, {Class, Error}, Stacktrace])),
|
[?MODULE, Info, {Class, Error}, Stacktrace])),
|
||||||
{noreply, State}
|
{noreply, State}
|
||||||
end.
|
end.
|
||||||
|
|
@ -85,22 +108,19 @@ handle_info(Info, State) ->
|
||||||
terminate(_Reason, State) ->
|
terminate(_Reason, State) ->
|
||||||
#state{xdb_fd = XdbFd} = State,
|
#state{xdb_fd = XdbFd} = State,
|
||||||
case is_pid(XdbFd) of
|
case is_pid(XdbFd) of
|
||||||
true ->
|
true -> file:close(XdbFd);
|
||||||
file:close(XdbFd);
|
_ -> skip
|
||||||
_ ->
|
|
||||||
skip
|
|
||||||
end,
|
end,
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
{ok, State}.
|
{ok, State}.
|
||||||
|
|
||||||
|
|
||||||
%%==========================================
|
%%==========================================
|
||||||
%% Internal function
|
%% Internal function
|
||||||
%% =========================================
|
%% =========================================
|
||||||
do_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) ->
|
do_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) ->
|
||||||
Reply = search_ip(IoDevice, Ip),
|
Reply = search_ip(IoDevice, Ip, State),
|
||||||
{reply, Reply, State};
|
{reply, Reply, State};
|
||||||
|
|
||||||
do_call(stop, _From, State) ->
|
do_call(stop, _From, State) ->
|
||||||
|
|
@ -108,7 +128,7 @@ do_call(stop, _From, State) ->
|
||||||
|
|
||||||
do_call(Request, From, State) ->
|
do_call(Request, From, State) ->
|
||||||
error_logger:error_report(io_lib:format("unknown request: ~p, from:~p", [Request, From])),
|
error_logger:error_report(io_lib:format("unknown request: ~p, from:~p", [Request, From])),
|
||||||
{noreply, State}.
|
{reply, {error, unknown_request}, State}.
|
||||||
|
|
||||||
do_cast(Msg, State) ->
|
do_cast(Msg, State) ->
|
||||||
error_logger:error_report(io_lib:format("unknown msg: ~p", [Msg])),
|
error_logger:error_report(io_lib:format("unknown msg: ~p", [Msg])),
|
||||||
|
|
@ -117,67 +137,105 @@ do_cast(Msg, State) ->
|
||||||
do_info(Info, State) ->
|
do_info(Info, State) ->
|
||||||
error_logger:error_report(io:format("unknown info: ~p", [Info])),
|
error_logger:error_report(io:format("unknown info: ~p", [Info])),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
|
|
||||||
load_vector_index(IoDevice) ->
|
-spec resolve_version(#xdb_header{}) -> {ok, ipv4 | ipv6} | {error, term()}.
|
||||||
Key = ip2region_header_loaded,
|
resolve_version(Header) ->
|
||||||
case persistent_term:get(Key, false) of
|
case ip2region_xdb:header_version(Header) of
|
||||||
true -> ok;
|
2 -> {ok, ipv4};
|
||||||
_ ->
|
3 ->
|
||||||
{ok, <<_Header:?XDB_HEADER_SIZE/binary, VectorIndexBin/binary>> } =
|
case ip2region_xdb:header_ip_version(Header) of
|
||||||
file:read(IoDevice, ?XDB_HEADER_SIZE + ?XDB_VECTOR_INDEX_COUNT*8),
|
?IP_VERSION_4 -> {ok, ipv4};
|
||||||
load_vector_index_aux(VectorIndexBin, 0),
|
?IP_VERSION_6 -> {ok, ipv6};
|
||||||
persistent_term:put(Key, true)
|
V -> {error, {invalid_xdb_ip_version, V}}
|
||||||
|
end;
|
||||||
|
V -> {error, {invalid_xdb_version, V}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
load_vector_index_aux(<<>>, _Index) -> ok;
|
load_vector_index(IoDevice, Version) ->
|
||||||
load_vector_index_aux(<<SPtr:32/little, EPtr:32/little, VectorIndexBin/binary>>, Index) ->
|
%% The vector index ETS table is global and shared by all pool workers.
|
||||||
Term = {Index, SPtr, EPtr},
|
%% Only the first worker (or the first worker after a restart) populates it.
|
||||||
ets:insert(?XDB_VECTOR_INDEX, Term),
|
%% Subsequent workers skip the file:read here; search_ip/2 uses file:pread/3,
|
||||||
load_vector_index_aux(VectorIndexBin, Index + 1).
|
%% so the current file pointer position does not affect correctness.
|
||||||
|
Table = ip2region_xdb:vector_index_table(Version),
|
||||||
|
case ets:info(Table, size) of
|
||||||
search_ip(IoDevice, Ip) ->
|
?XDB_VECTOR_INDEX_COUNT ->
|
||||||
IntIp = ip2region_util:ipv4_to_n(Ip),
|
ok;
|
||||||
case ets:lookup(?IP2REGION_CACHE, IntIp) of
|
undefined ->
|
||||||
[{_IntIp, RegionInfo}] ->
|
Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}],
|
||||||
RegionInfo;
|
ets:new(Table, Opts),
|
||||||
|
load_vector_index_data(IoDevice, Table);
|
||||||
_ ->
|
_ ->
|
||||||
<<A:8, B:8, _Rest/binary>> = <<IntIp:32>>,
|
%% A previous worker may have crashed midway; reload to be safe.
|
||||||
VectorIdx = A * ?XDB_VECTOR_COLS + B,
|
load_vector_index_data(IoDevice, Table)
|
||||||
[{_, SPtr, EPtr}] = ets:lookup(?XDB_VECTOR_INDEX, VectorIdx),
|
|
||||||
RegionInfo = search_ip(IoDevice, IntIp, SPtr, EPtr, 0, (EPtr - SPtr) div ?XDB_SEGMENT_INDEX_SIZE),
|
|
||||||
ets:insert_new(?IP2REGION_CACHE, {IntIp, RegionInfo}),
|
|
||||||
RegionInfo
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
search_ip(IoDevice, IntIp, SPtr, EPtr, Low, High) when Low =< High ->
|
load_vector_index_data(IoDevice, Table) ->
|
||||||
|
{ok, VectorIndexBin} =
|
||||||
|
file:read(IoDevice, ?XDB_VECTOR_INDEX_COUNT * ?XDB_VECTOR_INDEX_SIZE),
|
||||||
|
load_vector_index_aux(VectorIndexBin, 0, Table).
|
||||||
|
|
||||||
|
load_vector_index_aux(<<>>, _Index, _Table) -> ok;
|
||||||
|
load_vector_index_aux(<<SPtr:32/little, EPtr:32/little, VectorIndexBin/binary>>, Index, Table) ->
|
||||||
|
ets:insert(Table, {Index, SPtr, EPtr}),
|
||||||
|
load_vector_index_aux(VectorIndexBin, Index + 1, Table).
|
||||||
|
|
||||||
|
search_ip(IoDevice, Ip, #state{version = Version}) ->
|
||||||
|
SegSize = ip2region_xdb:segment_index_size(Version),
|
||||||
|
VectorTable = ip2region_xdb:vector_index_table(Version),
|
||||||
|
SegmentTable = ip2region_xdb:segment_index_table(Version),
|
||||||
|
<<A:8, B:8, _/binary>> = Ip,
|
||||||
|
VectorIdx = A * ?XDB_VECTOR_COLS + B,
|
||||||
|
[{_, SPtr, EPtr}] = ets:lookup(VectorTable, VectorIdx),
|
||||||
|
search_ip(IoDevice, Ip, SPtr, EPtr, 0,
|
||||||
|
(EPtr - SPtr) div SegSize, SegSize, Version, SegmentTable).
|
||||||
|
|
||||||
|
search_ip(IoDevice, Ip, SPtr, EPtr, Low, High, SegSize, Version, SegmentTable) when Low =< High ->
|
||||||
Middle = (Low + High) bsr 1,
|
Middle = (Low + High) bsr 1,
|
||||||
SPtr2 = SPtr + Middle * ?XDB_SEGMENT_INDEX_SIZE,
|
SPtr2 = SPtr + Middle * SegSize,
|
||||||
{SIp, EIp, DataLen, DataPtr} = read_segement_index(IoDevice, SPtr2),
|
{SIp, EIp, DataLen, DataPtr} = read_segment_index(IoDevice, SPtr2, SegSize, SegmentTable),
|
||||||
if
|
case ip_in_range(Ip, SIp, EIp, Version) of
|
||||||
IntIp < SIp ->
|
below ->
|
||||||
search_ip(IoDevice, IntIp, SPtr, EPtr, Low, Middle - 1);
|
search_ip(IoDevice, Ip, SPtr, EPtr, Low, Middle - 1, SegSize, Version, SegmentTable);
|
||||||
IntIp > EIp ->
|
above ->
|
||||||
search_ip(IoDevice, IntIp, SPtr, EPtr, Middle + 1, High);
|
search_ip(IoDevice, Ip, SPtr, EPtr, Middle + 1, High, SegSize, Version, SegmentTable);
|
||||||
true ->
|
inside ->
|
||||||
{ok, DataBin} = read_file(IoDevice, DataPtr, DataLen),
|
{ok, DataBin} = read_file(IoDevice, DataPtr, DataLen),
|
||||||
unicode:characters_to_nfc_list(DataBin)
|
unicode:characters_to_nfc_list(DataBin)
|
||||||
end;
|
end;
|
||||||
search_ip(_IoDevice, _IntIp, _SPtr, _EPtr, _Low, _High) ->
|
search_ip(_IoDevice, _Ip, _SPtr, _EPtr, _Low, _High, _SegSize, _Version, _SegmentTable) ->
|
||||||
{error, unknown}.
|
{error, unknown}.
|
||||||
|
|
||||||
read_file(IoDevice, Position, DataLength) ->
|
ip_in_range(Ip, SIp, EIp, ipv4) ->
|
||||||
file:position(IoDevice, {bof, Position}),
|
<<InputInt:32>> = Ip,
|
||||||
file:read(IoDevice, DataLength).
|
if
|
||||||
|
InputInt < SIp -> below;
|
||||||
|
InputInt > EIp -> above;
|
||||||
|
true -> inside
|
||||||
|
end;
|
||||||
|
ip_in_range(Ip, SIp, EIp, ipv6) ->
|
||||||
|
if
|
||||||
|
Ip < SIp -> below;
|
||||||
|
Ip > EIp -> above;
|
||||||
|
true -> inside
|
||||||
|
end.
|
||||||
|
|
||||||
read_segement_index(IoDevice, SPtr) ->
|
read_file(IoDevice, Position, DataLength) ->
|
||||||
case ets:lookup(?XDB_SEGMENT_INDEX, SPtr) of
|
file:pread(IoDevice, Position, DataLength).
|
||||||
|
|
||||||
|
read_segment_index(IoDevice, SPtr, SegSize, SegmentTable) ->
|
||||||
|
case ets:lookup(SegmentTable, SPtr) of
|
||||||
[{_SPtr, SIp, EIp, DataLen, DataPtr}] ->
|
[{_SPtr, SIp, EIp, DataLen, DataPtr}] ->
|
||||||
{SIp, EIp, DataLen, DataPtr};
|
{SIp, EIp, DataLen, DataPtr};
|
||||||
_ ->
|
_ ->
|
||||||
{ok, <<SIp:32/little, EIp:32/little, DataLen:16/little, DataPtr:32/little>>} =
|
{ok, Bin} = read_file(IoDevice, SPtr, SegSize),
|
||||||
read_file(IoDevice, SPtr, ?XDB_SEGMENT_INDEX_SIZE),
|
{SIp, EIp, DataLen, DataPtr} = decode_segment_index(Bin, SegSize),
|
||||||
ets:insert_new(?XDB_SEGMENT_INDEX, {SPtr, SIp, EIp, DataLen, DataPtr}),
|
ets:insert_new(SegmentTable, {SPtr, SIp, EIp, DataLen, DataPtr}),
|
||||||
{SIp, EIp, DataLen, DataPtr}
|
{SIp, EIp, DataLen, DataPtr}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V4) ->
|
||||||
|
<<SIp:32/little, EIp:32/little, DataLen:16/little, DataPtr:32/little>> = Bin,
|
||||||
|
{SIp, EIp, DataLen, DataPtr};
|
||||||
|
decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V6) ->
|
||||||
|
<<SIp:16/binary, EIp:16/binary, DataLen:16/little, DataPtr:32/little>> = Bin,
|
||||||
|
{SIp, EIp, DataLen, DataPtr}.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
%% Use of this source code is governed by a Apache2.0-style
|
||||||
|
%% license that can be found in the LICENSE file.
|
||||||
|
%%
|
||||||
|
%% @doc
|
||||||
|
%% ip2region xdb binary format helpers.
|
||||||
|
%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(ip2region_xdb).
|
||||||
|
|
||||||
|
-export([parse_header/1]).
|
||||||
|
-export([header_version/1, header_index_policy/1, header_created_at/1,
|
||||||
|
header_start_index_ptr/1, header_end_index_ptr/1,
|
||||||
|
header_ip_version/1, header_runtime_ptr_bytes/1]).
|
||||||
|
-export([segment_index_size/1]).
|
||||||
|
-export([vector_index_table/1, segment_index_table/1, cache_table/1]).
|
||||||
|
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
-spec parse_header(binary()) -> {ok, #xdb_header{}} | {error, invalid_header}.
|
||||||
|
parse_header(Bin) when is_binary(Bin), byte_size(Bin) >= 20 ->
|
||||||
|
<<Version:16/little, IndexPolicy:16/little, CreatedAt:32/little,
|
||||||
|
StartIndexPtr:32/little, EndIndexPtr:32/little,
|
||||||
|
IpVersion:16/little, RuntimePtrBytes:16/little, _/binary>> = Bin,
|
||||||
|
{ok, #xdb_header{
|
||||||
|
version = Version,
|
||||||
|
index_policy = IndexPolicy,
|
||||||
|
created_at = CreatedAt,
|
||||||
|
start_index_ptr = StartIndexPtr,
|
||||||
|
end_index_ptr = EndIndexPtr,
|
||||||
|
ip_version = IpVersion,
|
||||||
|
runtime_ptr_bytes = RuntimePtrBytes
|
||||||
|
}};
|
||||||
|
parse_header(_) ->
|
||||||
|
{error, invalid_header}.
|
||||||
|
|
||||||
|
header_version(#xdb_header{version = V}) -> V.
|
||||||
|
header_index_policy(#xdb_header{index_policy = P}) -> P.
|
||||||
|
header_created_at(#xdb_header{created_at = T}) -> T.
|
||||||
|
header_start_index_ptr(#xdb_header{start_index_ptr = P}) -> P.
|
||||||
|
header_end_index_ptr(#xdb_header{end_index_ptr = P}) -> P.
|
||||||
|
header_ip_version(#xdb_header{ip_version = V}) -> V.
|
||||||
|
header_runtime_ptr_bytes(#xdb_header{runtime_ptr_bytes = B}) -> B.
|
||||||
|
|
||||||
|
-spec segment_index_size(ipv4 | ipv6) -> pos_integer().
|
||||||
|
segment_index_size(ipv4) -> ?XDB_SEGMENT_INDEX_SIZE_V4;
|
||||||
|
segment_index_size(ipv6) -> ?XDB_SEGMENT_INDEX_SIZE_V6.
|
||||||
|
|
||||||
|
-spec vector_index_table(ipv4 | ipv6) -> atom().
|
||||||
|
vector_index_table(ipv4) -> ?XDB_VECTOR_INDEX_V4;
|
||||||
|
vector_index_table(ipv6) -> ?XDB_VECTOR_INDEX_V6.
|
||||||
|
|
||||||
|
-spec segment_index_table(ipv4 | ipv6) -> atom().
|
||||||
|
segment_index_table(ipv4) -> ?XDB_SEGMENT_INDEX_V4;
|
||||||
|
segment_index_table(ipv6) -> ?XDB_SEGMENT_INDEX_V6.
|
||||||
|
|
||||||
|
-spec cache_table(ipv4 | ipv6) -> atom().
|
||||||
|
cache_table(ipv4) -> ?IP2REGION_CACHE_V4;
|
||||||
|
cache_table(ipv6) -> ?IP2REGION_CACHE_V6.
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
%% Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
%% Use of this source code is governed by a Apache2.0-style
|
%% Use of this source code is governed by a Apache2.0-style
|
||||||
%% license that can be found in the LICENSE file.
|
%% license that can be found in the LICENSE file.
|
||||||
%%
|
%%
|
||||||
%% @doc
|
%% @doc
|
||||||
%% ip2region xdb client search api
|
%% ip2region xdb client search api with IPv4/IPv6 auto-routing.
|
||||||
%% @end
|
%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(xdb).
|
-module(xdb).
|
||||||
|
|
@ -13,20 +12,37 @@
|
||||||
|
|
||||||
-export([search/1]).
|
-export([search/1]).
|
||||||
|
|
||||||
-spec search(Ip :: tuple() | list() | binary()) -> Result :: binary | {error, Reason::atom()}.
|
-spec search(Ip :: tuple() | list() | binary() | integer()) ->
|
||||||
search(Ip) when is_integer(Ip); is_list(Ip); is_tuple(Ip); is_binary(Ip) ->
|
Result :: list() | {error, term()}.
|
||||||
case ip2region_util:ipv4_to_n(Ip) of
|
search(Ip) ->
|
||||||
IntIp when is_integer(IntIp) ->
|
case ip2region_util:ip_to_bytes(Ip) of
|
||||||
case ets:lookup(?IP2REGION_CACHE, IntIp) of
|
{ok, ipv4, IpBin} ->
|
||||||
[{_IntIp, Region}] -> Region;
|
do_search(?IP2REGION_POOL_V4, ipv4, IpBin);
|
||||||
_ ->
|
{ok, ipv6, IpBin} ->
|
||||||
Worker = poolboy:checkout(?IP2REGION_POOL, true, infinity),
|
do_search(?IP2REGION_POOL_V6, ipv6, IpBin);
|
||||||
try
|
Ret ->
|
||||||
ip2region_worker:search(Worker, IntIp)
|
Ret
|
||||||
after
|
end.
|
||||||
poolboy:checkin(?IP2REGION_POOL, Worker)
|
|
||||||
end
|
do_search(PoolName, Version, IpBin) ->
|
||||||
end;
|
CacheTable = ip2region_xdb:cache_table(Version),
|
||||||
Ret ->
|
case ets:lookup(CacheTable, IpBin) of
|
||||||
Ret
|
[{_, Region}] -> Region;
|
||||||
|
_ ->
|
||||||
|
case whereis(PoolName) of
|
||||||
|
undefined ->
|
||||||
|
{error, pool_not_configured};
|
||||||
|
_ ->
|
||||||
|
Worker = poolboy:checkout(PoolName, true, infinity),
|
||||||
|
try
|
||||||
|
case ip2region_worker:search(Worker, IpBin) of
|
||||||
|
{error, _} = Err -> Err;
|
||||||
|
Region ->
|
||||||
|
ets:insert(CacheTable, {IpBin, Region}),
|
||||||
|
Region
|
||||||
|
end
|
||||||
|
after
|
||||||
|
poolboy:checkin(PoolName, Worker)
|
||||||
|
end
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
|
||||||
|
|
@ -11,29 +11,84 @@
|
||||||
-export([main/1]).
|
-export([main/1]).
|
||||||
|
|
||||||
main(DataFile) ->
|
main(DataFile) ->
|
||||||
application:ensure_started(ip2region),
|
%% Keep benchmark output clean while still surfacing real errors.
|
||||||
|
_ = logger:set_handler_config(default, level, error),
|
||||||
|
_ = logger:set_primary_config(level, error),
|
||||||
|
{ok, _} = application:ensure_all_started(ip2region),
|
||||||
show_hw_sw_info(),
|
show_hw_sw_info(),
|
||||||
IpList = load_test_data(DataFile),
|
IpList = load_test_data(DataFile),
|
||||||
run(IpList).
|
run(IpList).
|
||||||
|
|
||||||
show_hw_sw_info() ->
|
show_hw_sw_info() ->
|
||||||
io:format("CPU info:~n", []),
|
{Model, Clock, Cores, Threads} = cpu_info(),
|
||||||
io:format("~s", [os:cmd("egrep '^model name' /proc/cpuinfo | head -1")]),
|
io:format("~nSystem:~n", []),
|
||||||
io:format("~s", [os:cmd("egrep '^cache' /proc/cpuinfo | head -1")]),
|
io:format(" CPU : ~s", [Model]),
|
||||||
io:format("~s", [os:cmd("egrep '^cpu MHz' /proc/cpuinfo | head -1")]),
|
case Clock of
|
||||||
io:format("~s", [os:cmd("egrep '^bogomips' /proc/cpuinfo | head -1")]),
|
"" -> io:format("~n", []);
|
||||||
io:format("cores/threads : ~s~n", [os:cmd("egrep -c '^processor' /proc/cpuinfo")]),
|
_ -> io:format(" @ ~s~n", [Clock])
|
||||||
io:format("Erlang info:~n", []),
|
end,
|
||||||
io:format("system_version:~s", [erlang:system_info(system_version)]),
|
io:format(" Cores : ~s cores / ~s threads~n", [Cores, Threads]),
|
||||||
|
io:format(" Erlang : ~s~n", [string:trim(erlang:system_info(system_version))]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
cpu_info() ->
|
||||||
|
case os:type() of
|
||||||
|
{unix, darwin} ->
|
||||||
|
Model = sysctl("machdep.cpu.brand_string"),
|
||||||
|
Clock = first_non_empty([
|
||||||
|
format_clock(sysctl("hw.cpufrequency")),
|
||||||
|
format_clock(sysctl("hw.perflevel0.frequency")),
|
||||||
|
format_clock(sysctl("hw.perflevel1.frequency"))
|
||||||
|
]),
|
||||||
|
Cores = sysctl("hw.physicalcpu"),
|
||||||
|
Threads = sysctl("hw.logicalcpu"),
|
||||||
|
{Model, Clock, Cores, Threads};
|
||||||
|
{unix, linux} ->
|
||||||
|
Model = linux_cpu_field("model name"),
|
||||||
|
Clock = format_clock_mhz(linux_cpu_field("cpu MHz")),
|
||||||
|
Cores = string:trim(os:cmd("grep -c '^processor' /proc/cpuinfo 2>/dev/null")),
|
||||||
|
Threads = Cores,
|
||||||
|
{Model, Clock, Cores, Threads};
|
||||||
|
_ ->
|
||||||
|
{"unknown", "", "?", "?"}
|
||||||
|
end.
|
||||||
|
|
||||||
|
sysctl(Key) ->
|
||||||
|
string:trim(os:cmd("sysctl -n " ++ Key ++ " 2>/dev/null")).
|
||||||
|
|
||||||
|
linux_cpu_field(Key) ->
|
||||||
|
Cmd = "grep -m1 '^" ++ Key ++ "' /proc/cpuinfo 2>/dev/null | cut -d: -f2- | sed 's/^ *//'",
|
||||||
|
string:trim(os:cmd(Cmd)).
|
||||||
|
|
||||||
|
first_non_empty(["" | Rest]) -> first_non_empty(Rest);
|
||||||
|
first_non_empty([Val | _]) -> Val;
|
||||||
|
first_non_empty([]) -> "".
|
||||||
|
|
||||||
|
format_clock(HzStr) ->
|
||||||
|
case string:to_integer(string:trim(HzStr)) of
|
||||||
|
{ok, Hz, _} when Hz > 1000000000 ->
|
||||||
|
lists:flatten(io_lib:format("~.2f GHz", [Hz / 1000000000]));
|
||||||
|
{ok, Hz, _} when Hz > 1000000 ->
|
||||||
|
lists:flatten(io_lib:format("~.2f MHz", [Hz / 1000000]));
|
||||||
|
_ ->
|
||||||
|
""
|
||||||
|
end.
|
||||||
|
|
||||||
|
format_clock_mhz(MhzStr) ->
|
||||||
|
case string:to_float(string:trim(MhzStr)) of
|
||||||
|
{ok, Mhz, _} ->
|
||||||
|
lists:flatten(io_lib:format("~.3f GHz", [Mhz / 1000]));
|
||||||
|
_ ->
|
||||||
|
""
|
||||||
|
end.
|
||||||
|
|
||||||
load_test_data(DataFile) ->
|
load_test_data(DataFile) ->
|
||||||
{ok, Fd} = file:open(DataFile, [read]),
|
{ok, Fd} = file:open(DataFile, [read]),
|
||||||
T0 = os:timestamp(),
|
T0 = os:timestamp(),
|
||||||
IpList = load_test_data(Fd, []),
|
IpList = load_test_data(Fd, []),
|
||||||
T1 = os:timestamp(),
|
T1 = os:timestamp(),
|
||||||
Sec = timer:now_diff(T1, T0) / 1000000,
|
Sec = timer:now_diff(T1, T0) / 1000000,
|
||||||
io:format("load test data use ~ps~n", [Sec]),
|
io:format(" Loaded : ~p IPs in ~.3f s~n", [length(IpList), Sec]),
|
||||||
IpList.
|
IpList.
|
||||||
|
|
||||||
load_test_data(Fd, IpList) ->
|
load_test_data(Fd, IpList) ->
|
||||||
|
|
@ -46,30 +101,30 @@ load_test_data(Fd, IpList) ->
|
||||||
load_test_data(Fd, IpList)
|
load_test_data(Fd, IpList)
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
file:close(Fd),
|
file:close(Fd),
|
||||||
IpList
|
IpList
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run(IpList) ->
|
run(IpList) ->
|
||||||
garbage_collect(),
|
garbage_collect(),
|
||||||
io:format("~nstart run benchmark tests~n", []),
|
io:format("~nBenchmarks:~n", []),
|
||||||
io:format("~nsearch from file:~n", []),
|
run_test("cold", IpList),
|
||||||
run_test(IpList),
|
run_test("warm", IpList),
|
||||||
io:format("~nsearch from cache:~n", []),
|
io:format("~nDone.~n", []).
|
||||||
run_test(IpList),
|
|
||||||
io:format("~nbenchmark test finish~n", []).
|
|
||||||
|
|
||||||
run_test(IpList) ->
|
run_test(Label, IpList) ->
|
||||||
T0 = os:timestamp(),
|
T0 = os:timestamp(),
|
||||||
run_test_aux(IpList),
|
run_test_aux(IpList),
|
||||||
T1 = os:timestamp(),
|
T1 = os:timestamp(),
|
||||||
Sec = timer:now_diff(T1, T0) / 1000000,
|
Sec = timer:now_diff(T1, T0) / 1000000,
|
||||||
IpCount = length(IpList),
|
Count = length(IpList),
|
||||||
io:format("ip count:~p,~ntotal time: ~ps,~nsearch ~p times per second,~nuse ~p micro second per search~n",
|
Qps = Count / Sec,
|
||||||
[IpCount, Sec, IpCount / Sec, Sec * 1000000/IpCount]).
|
MsOp = Sec * 1000 / Count,
|
||||||
|
UsOp = Sec * 1000000 / Count,
|
||||||
|
io:format(" ~-8s total=~7.3fs count=~7w qps=~12.2f avg=~9.6f ms/op (~6.3f us/op)~n",
|
||||||
|
[Label, Sec, Count, Qps, MsOp, UsOp]).
|
||||||
|
|
||||||
run_test_aux([]) -> ok;
|
run_test_aux([]) -> ok;
|
||||||
run_test_aux([Ip | Tail]) ->
|
run_test_aux([Ip | Tail]) ->
|
||||||
xdb:search(Ip),
|
xdb:search(Ip),
|
||||||
run_test_aux(Tail).
|
run_test_aux(Tail).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
-module(ip2region_sup_test).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
pools_started_test() ->
|
||||||
|
{ok, _} = ip2region_test_util:start_app(),
|
||||||
|
?assert(is_pid(whereis(?IP2REGION_POOL_V4))),
|
||||||
|
?assert(is_pid(whereis(?IP2REGION_POOL_V6))).
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
-module(ip2region_test_util).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
repo_root/0,
|
||||||
|
v6_xdb_path/0,
|
||||||
|
default_db_config/0,
|
||||||
|
reset_app/0,
|
||||||
|
load_app/0,
|
||||||
|
set_db_config/1,
|
||||||
|
start_app/0,
|
||||||
|
start_app/1,
|
||||||
|
stop_app/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
repo_root() ->
|
||||||
|
TestDir = filename:dirname(?FILE),
|
||||||
|
ErlangDir = filename:dirname(TestDir),
|
||||||
|
BindingDir = filename:dirname(ErlangDir),
|
||||||
|
filename:dirname(BindingDir).
|
||||||
|
|
||||||
|
v6_xdb_path() ->
|
||||||
|
filename:join([repo_root(), "data", "ip2region_v6.xdb"]).
|
||||||
|
|
||||||
|
default_db_config() ->
|
||||||
|
[
|
||||||
|
{ipv4, "ip2region.xdb"},
|
||||||
|
{ipv6, v6_xdb_path()}
|
||||||
|
].
|
||||||
|
|
||||||
|
reset_app() ->
|
||||||
|
try application:stop(ip2region) catch _:_ -> ok end,
|
||||||
|
try application:unload(ip2region) catch _:_ -> ok end,
|
||||||
|
ok.
|
||||||
|
|
||||||
|
load_app() ->
|
||||||
|
ok = application:load(ip2region).
|
||||||
|
|
||||||
|
set_db_config(Config) ->
|
||||||
|
ok = application:set_env(ip2region, db, Config).
|
||||||
|
|
||||||
|
start_app() ->
|
||||||
|
start_app(default_db_config()).
|
||||||
|
|
||||||
|
start_app(Config) ->
|
||||||
|
reset_app(),
|
||||||
|
load_app(),
|
||||||
|
set_db_config(Config),
|
||||||
|
application:ensure_all_started(ip2region).
|
||||||
|
|
||||||
|
stop_app() ->
|
||||||
|
application:stop(ip2region).
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
-module(ip2region_util_test).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
ipv4_inputs_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes("1.0.8.0")),
|
||||||
|
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes(<<"1.0.8.0">>)),
|
||||||
|
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes({1,0,8,0})),
|
||||||
|
?_assertEqual({ok, ipv4, <<1,0,8,0>>}, ip2region_util:ip_to_bytes(16779264)),
|
||||||
|
?_assertEqual(ipv4, ip2region_util:ip_version("1.0.8.0")),
|
||||||
|
?_assertEqual(ipv4, ip2region_util:ip_version({1,0,8,0}))
|
||||||
|
].
|
||||||
|
|
||||||
|
ipv6_inputs_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1>>},
|
||||||
|
ip2region_util:ip_to_bytes("::1")),
|
||||||
|
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,255,255,1,0,8,0>>},
|
||||||
|
ip2region_util:ip_to_bytes("::ffff:1.0.8.0")),
|
||||||
|
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,255,255,1,0,8,0>>},
|
||||||
|
ip2region_util:ip_to_bytes(<<"::ffff:1.0.8.0">>)),
|
||||||
|
?_assertEqual({ok, ipv6, <<0,0,0,0,0,0,0,0,0,0,255,255,1,0,8,0>>},
|
||||||
|
ip2region_util:ip_to_bytes({0,0,0,0,0,65535,256,2048})),
|
||||||
|
?_assertEqual(ipv6, ip2region_util:ip_version("::1")),
|
||||||
|
?_assertEqual(ipv6, ip2region_util:ip_version({0,0,0,0,0,0,0,1}))
|
||||||
|
].
|
||||||
|
|
||||||
|
invalid_inputs_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes("xxx.0.8.0")),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes("::ggg")),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({1,2,3})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({1,2,3,4,5,6,7})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes(atom)),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes(-1)),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes(16#100000000)),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({256, 0, 0, 1})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_to_bytes({-1, 0, 0, 0})),
|
||||||
|
?_assertEqual({error, bad_ip_format},
|
||||||
|
ip2region_util:ip_to_bytes({0, 0, 0, 0, 0, 0, 0, 65536})),
|
||||||
|
?_assertEqual({error, bad_ip_format},
|
||||||
|
ip2region_util:ip_to_bytes({0, 0, 0, 0, 0, 0, 0, -1})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n(-1)),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n(16#100000000)),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n({256, 0, 0, 1}))
|
||||||
|
].
|
||||||
|
|
||||||
|
ip_version_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual(ipv4, ip2region_util:ip_version(<<"1.0.8.0">>)),
|
||||||
|
?_assertEqual(ipv6, ip2region_util:ip_version(<<"::1">>)),
|
||||||
|
?_assertEqual(ipv4, ip2region_util:ip_version(16779264 + 1)),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version("::ggg")),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version({1,2,3})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version({256,0,0,1})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version({0,0,0,0,0,0,0,65536})),
|
||||||
|
?_assertEqual({error, bad_ip_format}, ip2region_util:ip_version(16#100000000))
|
||||||
|
].
|
||||||
|
|
||||||
|
legacy_ipv4_to_n_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual(16779264, ip2region_util:ipv4_to_n("1.0.8.0")),
|
||||||
|
?_assertEqual(16779264, ip2region_util:ipv4_to_n(<<"1.0.8.0">>)),
|
||||||
|
?_assertEqual(16779264, ip2region_util:ipv4_to_n({1,0,8,0})),
|
||||||
|
?_assertEqual(16779264, ip2region_util:ipv4_to_n(16779264))
|
||||||
|
].
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
-module(ip2region_worker_test).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
worker_search_v4_binary_test() ->
|
||||||
|
ip2region_sup:create_table(),
|
||||||
|
{ok, Pid} = ip2region_worker:start_link([{xdb_file, "ip2region.xdb"}]),
|
||||||
|
try
|
||||||
|
Region = ip2region_worker:search(Pid, <<1,0,8,0>>),
|
||||||
|
?assert(is_list(Region))
|
||||||
|
after
|
||||||
|
ip2region_worker:stop(Pid)
|
||||||
|
end.
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
-module(ip2region_xdb_test).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-include("ip2region.hrl").
|
||||||
|
|
||||||
|
valid_v4_header_test() ->
|
||||||
|
HeaderBin = <<2:16/little, 1:16/little, 0:32/little,
|
||||||
|
256:32/little, 1024:32/little,
|
||||||
|
?IP_VERSION_4:16/little, 4:16/little>>,
|
||||||
|
{ok, Header} = ip2region_xdb:parse_header(HeaderBin),
|
||||||
|
?assertEqual(2, ip2region_xdb:header_version(Header)),
|
||||||
|
?assertEqual(1, ip2region_xdb:header_index_policy(Header)),
|
||||||
|
?assertEqual(256, ip2region_xdb:header_start_index_ptr(Header)),
|
||||||
|
?assertEqual(1024, ip2region_xdb:header_end_index_ptr(Header)),
|
||||||
|
?assertEqual(?IP_VERSION_4, ip2region_xdb:header_ip_version(Header)),
|
||||||
|
?assertEqual(4, ip2region_xdb:header_runtime_ptr_bytes(Header)).
|
||||||
|
|
||||||
|
valid_v6_header_test() ->
|
||||||
|
HeaderBin = <<3:16/little, 1:16/little, 0:32/little,
|
||||||
|
256:32/little, 1024:32/little,
|
||||||
|
?IP_VERSION_6:16/little, 4:16/little>>,
|
||||||
|
{ok, Header} = ip2region_xdb:parse_header(HeaderBin),
|
||||||
|
?assertEqual(3, ip2region_xdb:header_version(Header)),
|
||||||
|
?assertEqual(?IP_VERSION_6, ip2region_xdb:header_ip_version(Header)).
|
||||||
|
|
||||||
|
invalid_header_test() ->
|
||||||
|
?assertEqual({error, invalid_header}, ip2region_xdb:parse_header(<<0:128>>)).
|
||||||
|
|
||||||
|
table_helpers_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual(?XDB_VECTOR_INDEX_V4, ip2region_xdb:vector_index_table(ipv4)),
|
||||||
|
?_assertEqual(?XDB_VECTOR_INDEX_V6, ip2region_xdb:vector_index_table(ipv6)),
|
||||||
|
?_assertEqual(?XDB_SEGMENT_INDEX_V4, ip2region_xdb:segment_index_table(ipv4)),
|
||||||
|
?_assertEqual(?XDB_SEGMENT_INDEX_V6, ip2region_xdb:segment_index_table(ipv6)),
|
||||||
|
?_assertEqual(?IP2REGION_CACHE_V4, ip2region_xdb:cache_table(ipv4)),
|
||||||
|
?_assertEqual(?IP2REGION_CACHE_V6, ip2region_xdb:cache_table(ipv6))
|
||||||
|
].
|
||||||
|
|
||||||
|
segment_index_size_test_() ->
|
||||||
|
[
|
||||||
|
?_assertEqual(14, ip2region_xdb:segment_index_size(ipv4)),
|
||||||
|
?_assertEqual(38, ip2region_xdb:segment_index_size(ipv6))
|
||||||
|
].
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(IPV6_RESULT, "United States|Florida|Miami|Google LLC|US").
|
||||||
|
|
||||||
search_test_() ->
|
search_test_() ->
|
||||||
application:ensure_started(ip2region),
|
{ok, _} = ip2region_test_util:start_app(),
|
||||||
A = "中国|0|广东省|广州市|电信",
|
A = "中国|广东省|广州市|中国电信|CN",
|
||||||
Region0 = xdb:search("1.0.8.0"),
|
Region0 = xdb:search("1.0.8.0"),
|
||||||
Region1 = xdb:search(<<"1.0.8.0">>),
|
Region1 = xdb:search(<<"1.0.8.0">>),
|
||||||
Region2 = xdb:search({1,0,8,0}),
|
Region2 = xdb:search({1,0,8,0}),
|
||||||
|
|
@ -14,5 +16,39 @@ search_test_() ->
|
||||||
?_assert(A =:= Region1),
|
?_assert(A =:= Region1),
|
||||||
?_assert(A =:= Region2),
|
?_assert(A =:= Region2),
|
||||||
?_assert({error, bad_ip_format} =:= Region3)
|
?_assert({error, bad_ip_format} =:= Region3)
|
||||||
|
].
|
||||||
|
|
||||||
].
|
ipv6_search_test_() ->
|
||||||
|
{ok, _} = ip2region_test_util:start_app(),
|
||||||
|
[
|
||||||
|
?_assert(?IPV6_RESULT =:= xdb:search("2001:4860:4860::8888")),
|
||||||
|
?_assert(?IPV6_RESULT =:= xdb:search(<<"2001:4860:4860::8888">>)),
|
||||||
|
?_assert(?IPV6_RESULT =:= xdb:search({8193, 18528, 18528, 0, 0, 0, 0, 34952}))
|
||||||
|
].
|
||||||
|
|
||||||
|
invalid_search_test_() ->
|
||||||
|
{ok, _} = ip2region_test_util:start_app(),
|
||||||
|
[
|
||||||
|
?_assertEqual({error, bad_ip_format}, xdb:search("xxx.0.8.0")),
|
||||||
|
?_assertEqual({error, bad_ip_format}, xdb:search("::ggg")),
|
||||||
|
?_assertEqual({error, bad_ip_format}, xdb:search({1,2,3}))
|
||||||
|
].
|
||||||
|
|
||||||
|
ipv6_pool_not_configured_test() ->
|
||||||
|
{ok, _} = ip2region_test_util:start_app([{ipv4, "ip2region.xdb"}]),
|
||||||
|
?assertEqual({error, pool_not_configured}, xdb:search("2001:4860:4860::8888")).
|
||||||
|
|
||||||
|
xdb_version_mismatch_test() ->
|
||||||
|
{ok, _} = ip2region_test_util:start_app(),
|
||||||
|
V4File = filename:join([
|
||||||
|
ip2region_test_util:repo_root(), "binding", "erlang", "priv", "ip2region.xdb"
|
||||||
|
]),
|
||||||
|
error_logger:tty(false),
|
||||||
|
try
|
||||||
|
?assertEqual(
|
||||||
|
{error, {xdb_version_mismatch, ipv6, ipv4}},
|
||||||
|
ip2region_worker:start([{xdb_file, V4File}, {expected_version, ipv6}])
|
||||||
|
)
|
||||||
|
after
|
||||||
|
error_logger:tty(true)
|
||||||
|
end.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue