From a3b8d817eec8f025b33678fdd782f28b240ef339 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sat, 27 Jun 2026 21:27:36 +0900 Subject: [PATCH 01/23] chore: ignore .worktrees directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ae58d2e..ab5f10d 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ target #vscode .vscode build + +# git worktrees +.worktrees/ From 211c374ee27086cf8cd8cc070291a1ad2207aaa1 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sat, 27 Jun 2026 23:50:19 +0900 Subject: [PATCH 02/23] feat(erlang): add xdb header and dual-stack table/pool macros --- binding/erlang/include/ip2region.hrl | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/binding/erlang/include/ip2region.hrl b/binding/erlang/include/ip2region.hrl index a0e91e7..2e7ea80 100644 --- a/binding/erlang/include/ip2region.hrl +++ b/binding/erlang/include/ip2region.hrl @@ -18,6 +18,36 @@ -define(IP2REGION_POOL, ip2region_pool). +%% xdb header field offsets (bytes) +-define(XDB_HEADER_VERSION_OFFSET, 0). +-define(XDB_HEADER_INDEX_POLICY_OFFSET, 2). +-define(XDB_HEADER_CREATED_AT_OFFSET, 4). +-define(XDB_HEADER_START_INDEX_PTR_OFFSET, 8). +-define(XDB_HEADER_END_INDEX_PTR_OFFSET, 12). +-define(XDB_HEADER_IP_VERSION_OFFSET, 16). +-define(XDB_HEADER_RUNTIME_PTR_BYTES_OFFSET, 18). +-define(XDB_HEADER_INFO_LENGTH, 256). + +%% IP versions as stored in xdb header +-define(IP_VERSION_4, 4). +-define(IP_VERSION_6, 6). + +%% 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. ?IP2REGION_POOL is kept as the legacy v4 pool name. +-define(IP2REGION_POOL_V4, ip2region_pool_v4). +-define(IP2REGION_POOL_V6, ip2region_pool_v6). + -ifndef(IF). -define(IF(C, T, F), case (C) of true -> (T); false -> (F) end). -define(IF(C, T), ?IF(C, T, skip)). From c1db56c369a0efcf106f4cd7d39a675091e26b40 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sat, 27 Jun 2026 23:51:07 +0900 Subject: [PATCH 03/23] feat(erlang): add xdb header parser helper module --- binding/erlang/src/ip2region_xdb.erl | 57 ++++++++++++++++++++++ binding/erlang/test/ip2region_xdb_test.erl | 16 ++++++ 2 files changed, 73 insertions(+) create mode 100644 binding/erlang/src/ip2region_xdb.erl create mode 100644 binding/erlang/test/ip2region_xdb_test.erl diff --git a/binding/erlang/src/ip2region_xdb.erl b/binding/erlang/src/ip2region_xdb.erl new file mode 100644 index 0000000..fc0f14e --- /dev/null +++ b/binding/erlang/src/ip2region_xdb.erl @@ -0,0 +1,57 @@ +%%%------------------------------------------------------------------- +%% 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]). + +-include("ip2region.hrl"). + +-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() +}). + +-spec parse_header(binary()) -> {ok, #xdb_header{}} | {error, invalid_header}. +parse_header(Bin) when is_binary(Bin), byte_size(Bin) >= 20 -> + <> = 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. diff --git a/binding/erlang/test/ip2region_xdb_test.erl b/binding/erlang/test/ip2region_xdb_test.erl new file mode 100644 index 0000000..1ce5a70 --- /dev/null +++ b/binding/erlang/test/ip2region_xdb_test.erl @@ -0,0 +1,16 @@ +-module(ip2region_xdb_test). +-include_lib("eunit/include/eunit.hrl"). + +header_parse_test() -> + HeaderBin = <<3:16/little, 1:16/little, 123456:32/little, + 1000:32/little, 2000:32/little, 6:16/little, 4:16/little, + 0:((256-20)*8)>>, + {ok, Header} = ip2region_xdb:parse_header(HeaderBin), + ?assertEqual(3, ip2region_xdb:header_version(Header)), + ?assertEqual(6, ip2region_xdb:header_ip_version(Header)), + ?assertEqual(1000, ip2region_xdb:header_start_index_ptr(Header)), + ?assertEqual(2000, ip2region_xdb:header_end_index_ptr(Header)). + +version_constants_test() -> + ?assertEqual(14, ip2region_xdb:segment_index_size(ipv4)), + ?assertEqual(38, ip2region_xdb:segment_index_size(ipv6)). From 2f0dc5f33fe31f5c2f8d1baa86e5cc14fb9d351d Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sat, 27 Jun 2026 23:52:40 +0900 Subject: [PATCH 04/23] feat(erlang): add dual-stack IP parsing utilities --- binding/erlang/src/ip2region_util.erl | 68 ++++++++++++++++----- binding/erlang/test/ip2region_util_test.erl | 42 +++++++++++++ 2 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 binding/erlang/test/ip2region_util_test.erl diff --git a/binding/erlang/src/ip2region_util.erl b/binding/erlang/src/ip2region_util.erl index 0359b85..df892f9 100644 --- a/binding/erlang/src/ip2region_util.erl +++ b/binding/erlang/src/ip2region_util.erl @@ -2,25 +2,65 @@ %% 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 utils +%% +%% @doc +%% ip2region utils: IPv4/IPv6 parsing and version detection. %% @end %%%------------------------------------------------------------------- -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; -ipv4_to_n({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) -> +-spec ip_version(Ip :: tuple() | list() | binary() | integer()) -> + ipv4 | ipv6 | {error, atom()}. +ip_version(Ip) when is_integer(Ip) -> ipv4; +ip_version({_, _, _, _}) -> ipv4; +ip_version({_, _, _, _, _, _, _, _}) -> 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 - {ok, Addr} -> - ipv4_to_n(Addr); + {ok, {_, _, _, _}} -> ipv4; + {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) -> + {ok, ipv4, <>}; +ip_to_bytes({A, B, C, D}) -> + {ok, ipv4, <>}; +ip_to_bytes({A, B, C, D, E, F, G, H}) -> + {ok, ipv6, <>}; +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, <>}; + {ok, {A, B, C, D, E, F, G, H}} -> + {ok, ipv6, <>}; _ -> {error, bad_ip_format} - end. \ No newline at end of file + 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; +ipv4_to_n({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; + _ -> {error, bad_ip_format} + end; +ipv4_to_n(_) -> + {error, bad_ip_format}. diff --git a/binding/erlang/test/ip2region_util_test.erl b/binding/erlang/test/ip2region_util_test.erl new file mode 100644 index 0000000..200f0a0 --- /dev/null +++ b/binding/erlang/test/ip2region_util_test.erl @@ -0,0 +1,42 @@ +-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(atom)) + ]. + +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)) + ]. From 241831a6f03cf8bb8d3b8715ba1b35107f17fa51 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 00:01:09 +0900 Subject: [PATCH 05/23] feat(erlang): make worker xdb version-aware for IPv4/IPv6 --- binding/erlang/src/ip2region_sup.erl | 20 +- binding/erlang/src/ip2region_worker.erl | 184 ++++++++++++------ binding/erlang/test/ip2region_worker_test.erl | 13 ++ 3 files changed, 156 insertions(+), 61 deletions(-) create mode 100644 binding/erlang/test/ip2region_worker_test.erl diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl index 304bc98..6aa6695 100644 --- a/binding/erlang/src/ip2region_sup.erl +++ b/binding/erlang/src/ip2region_sup.erl @@ -42,9 +42,23 @@ init([]) -> %% create_table() -> Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], - ets:new(?XDB_VECTOR_INDEX, Opts), - ets:new(?XDB_SEGMENT_INDEX, Opts), - ets:new(?IP2REGION_CACHE, Opts). + %% Legacy tables (kept for backward compatibility) + ensure_table(?XDB_VECTOR_INDEX, Opts), + ensure_table(?XDB_SEGMENT_INDEX, Opts), + ensure_table(?IP2REGION_CACHE, Opts), + %% Version-specific tables for dual-stack support + ensure_table(?XDB_VECTOR_INDEX_V4, 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). + +ensure_table(Name, Opts) -> + case ets:whereis(Name) of + undefined -> ets:new(Name, Opts); + _ -> ok + end. start_ip2region_pool(Sup) -> {ok, PoolArgsCfg} = application:get_env(poolargs), diff --git a/binding/erlang/src/ip2region_worker.erl b/binding/erlang/src/ip2region_worker.erl index 64d039c..e207635 100644 --- a/binding/erlang/src/ip2region_worker.erl +++ b/binding/erlang/src/ip2region_worker.erl @@ -2,9 +2,9 @@ %% 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 client worker +%% +%% @doc +%% ip2region xdb client worker, now version-aware (IPv4/IPv6). %% @end %%%------------------------------------------------------------------- -module(ip2region_worker). @@ -15,7 +15,12 @@ -export([start/1, stop/1, start_link/1]). -export([search/2]). -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, + segment_index_size :: pos_integer() +}). %%========================================== %% API @@ -28,7 +33,6 @@ start_link(Args) -> Opts = [{spawn_opt, [{min_heap_size, 6000}]}], gen_server:start_link(?MODULE, Args, Opts). - stop(Pid) -> gen_server:call(Pid, stop). @@ -38,26 +42,42 @@ search(Pid, Ip) -> %%========================================== %% gen_server callbacks %% ========================================= -init(_Args) -> +init(Args) -> process_flag(trap_exit, true), - AppName = + AppName = case application:get_application() of {ok, AName} -> AName; _ -> ?APP_NAME end, 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])), {ok, IoDevice} = file:open(XdbFileName, [read, binary]), - load_vector_index(IoDevice), - {ok, #state{xdb_fd = IoDevice}}. + {ok, HeaderBin} = file:read(IoDevice, ?XDB_HEADER_SIZE), + {ok, Header} = ip2region_xdb:parse_header(HeaderBin), + Version = resolve_version(Header), + SegmentIndexSize = ip2region_xdb:segment_index_size(Version), + load_vector_index(IoDevice, Version), + {ok, #state{ + xdb_fd = IoDevice, + version = Version, + segment_index_size = SegmentIndexSize + }}. handle_call(Request, From, State) -> try do_call(Request, From, State) catch 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])), {reply, {error, {Class, Error}}, State} end. @@ -67,7 +87,7 @@ handle_cast(Msg, State) -> do_cast(Msg, State) catch 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])), {noreply, State} end. @@ -77,7 +97,7 @@ handle_info(Info, State) -> do_info(Info, State) catch 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])), {noreply, State} end. @@ -85,22 +105,19 @@ handle_info(Info, State) -> terminate(_Reason, State) -> #state{xdb_fd = XdbFd} = State, case is_pid(XdbFd) of - true -> - file:close(XdbFd); - _ -> - skip + true -> file:close(XdbFd); + _ -> skip end, ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. - %%========================================== %% Internal function %% ========================================= do_call({search, Ip}, _From, #state{xdb_fd = IoDevice} = State) -> - Reply = search_ip(IoDevice, Ip), + Reply = search_ip(IoDevice, Ip, State), {reply, Reply, State}; do_call(stop, _From, State) -> @@ -117,67 +134,118 @@ do_cast(Msg, State) -> do_info(Info, State) -> error_logger:error_report(io:format("unknown info: ~p", [Info])), {noreply, State}. - -load_vector_index(IoDevice) -> - Key = ip2region_header_loaded, - case persistent_term:get(Key, false) of - true -> ok; - _ -> - {ok, <<_Header:?XDB_HEADER_SIZE/binary, VectorIndexBin/binary>> } = - file:read(IoDevice, ?XDB_HEADER_SIZE + ?XDB_VECTOR_INDEX_COUNT*8), - load_vector_index_aux(VectorIndexBin, 0), - persistent_term:put(Key, true) +resolve_version(Header) -> + case ip2region_xdb:header_version(Header) of + 2 -> ipv4; + 3 -> + case ip2region_xdb:header_ip_version(Header) of + ?IP_VERSION_4 -> ipv4; + ?IP_VERSION_6 -> ipv6; + _ -> ipv4 + end; + _ -> ipv4 end. -load_vector_index_aux(<<>>, _Index) -> ok; -load_vector_index_aux(<>, Index) -> - Term = {Index, SPtr, EPtr}, - ets:insert(?XDB_VECTOR_INDEX, Term), - load_vector_index_aux(VectorIndexBin, Index + 1). +load_vector_index(IoDevice, Version) -> + Table = vector_index_table(Version), + case ets:info(Table, size) of + undefined -> + Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], + ets:new(Table, Opts), + load_vector_index_data(IoDevice, Table); + 0 -> + load_vector_index_data(IoDevice, Table); + _ -> + ok + end. +load_vector_index_data(IoDevice, Table) -> + {ok, VectorIndexBin} = + file:read(IoDevice, ?XDB_VECTOR_INDEX_COUNT * 8), + load_vector_index_aux(VectorIndexBin, 0, Table). -search_ip(IoDevice, Ip) -> - IntIp = ip2region_util:ipv4_to_n(Ip), - case ets:lookup(?IP2REGION_CACHE, IntIp) of - [{_IntIp, RegionInfo}] -> +load_vector_index_aux(<<>>, _Index, _Table) -> ok; +load_vector_index_aux(<>, Index, Table) -> + ets:insert(Table, {Index, SPtr, EPtr}), + load_vector_index_aux(VectorIndexBin, Index + 1, Table). + +search_ip(IoDevice, IpInt, State) when is_integer(IpInt) -> + search_ip(IoDevice, <>, State); +search_ip(IoDevice, Ip, #state{version = Version, segment_index_size = SegSize}) -> + CacheTable = cache_table(Version), + VectorTable = vector_index_table(Version), + SegmentTable = segment_index_table(Version), + case ets:lookup(CacheTable, Ip) of + [{_, RegionInfo}] -> RegionInfo; _ -> - <> = <>, + <> = Ip, VectorIdx = A * ?XDB_VECTOR_COLS + B, - [{_, 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}), + [{_, SPtr, EPtr}] = ets:lookup(VectorTable, VectorIdx), + RegionInfo = search_ip(IoDevice, Ip, SPtr, EPtr, 0, + (EPtr - SPtr) div SegSize, SegSize, Version, SegmentTable), + ets:insert_new(CacheTable, {Ip, RegionInfo}), RegionInfo end. -search_ip(IoDevice, IntIp, SPtr, EPtr, Low, High) when Low =< High -> +search_ip(IoDevice, Ip, SPtr, EPtr, Low, High, SegSize, Version, SegmentTable) when Low =< High -> Middle = (Low + High) bsr 1, - SPtr2 = SPtr + Middle * ?XDB_SEGMENT_INDEX_SIZE, - {SIp, EIp, DataLen, DataPtr} = read_segement_index(IoDevice, SPtr2), - if - IntIp < SIp -> - search_ip(IoDevice, IntIp, SPtr, EPtr, Low, Middle - 1); - IntIp > EIp -> - search_ip(IoDevice, IntIp, SPtr, EPtr, Middle + 1, High); - true -> + SPtr2 = SPtr + Middle * SegSize, + {SIp, EIp, DataLen, DataPtr} = read_segment_index(IoDevice, SPtr2, SegSize, SegmentTable), + case ip_in_range(Ip, SIp, EIp, Version) of + below -> + search_ip(IoDevice, Ip, SPtr, EPtr, Low, Middle - 1, SegSize, Version, SegmentTable); + above -> + search_ip(IoDevice, Ip, SPtr, EPtr, Middle + 1, High, SegSize, Version, SegmentTable); + inside -> {ok, DataBin} = read_file(IoDevice, DataPtr, DataLen), unicode:characters_to_nfc_list(DataBin) end; -search_ip(_IoDevice, _IntIp, _SPtr, _EPtr, _Low, _High) -> +search_ip(_IoDevice, _Ip, _SPtr, _EPtr, _Low, _High, _SegSize, _Version, _SegmentTable) -> {error, unknown}. +ip_in_range(Ip, SIp, EIp, ipv4) -> + <> = Ip, + 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_file(IoDevice, Position, DataLength) -> file:position(IoDevice, {bof, Position}), file:read(IoDevice, DataLength). -read_segement_index(IoDevice, SPtr) -> - case ets:lookup(?XDB_SEGMENT_INDEX, SPtr) of +read_segment_index(IoDevice, SPtr, SegSize, SegmentTable) -> + case ets:lookup(SegmentTable, SPtr) of [{_SPtr, SIp, EIp, DataLen, DataPtr}] -> {SIp, EIp, DataLen, DataPtr}; _ -> - {ok, <>} = - read_file(IoDevice, SPtr, ?XDB_SEGMENT_INDEX_SIZE), - ets:insert_new(?XDB_SEGMENT_INDEX, {SPtr, SIp, EIp, DataLen, DataPtr}), + {ok, Bin} = read_file(IoDevice, SPtr, SegSize), + {SIp, EIp, DataLen, DataPtr} = decode_segment_index(Bin, SegSize), + ets:insert_new(SegmentTable, {SPtr, SIp, EIp, DataLen, DataPtr}), {SIp, EIp, DataLen, DataPtr} - end. \ No newline at end of file + end. + +decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V4) -> + <> = Bin, + {SIp, EIp, DataLen, DataPtr}; +decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V6) -> + <> = Bin, + {SIp, EIp, DataLen, DataPtr}. + +vector_index_table(ipv4) -> ?XDB_VECTOR_INDEX_V4; +vector_index_table(ipv6) -> ?XDB_VECTOR_INDEX_V6. + +segment_index_table(ipv4) -> ?XDB_SEGMENT_INDEX_V4; +segment_index_table(ipv6) -> ?XDB_SEGMENT_INDEX_V6. + +cache_table(ipv4) -> ?IP2REGION_CACHE_V4; +cache_table(ipv6) -> ?IP2REGION_CACHE_V6. diff --git a/binding/erlang/test/ip2region_worker_test.erl b/binding/erlang/test/ip2region_worker_test.erl new file mode 100644 index 0000000..5a2417c --- /dev/null +++ b/binding/erlang/test/ip2region_worker_test.erl @@ -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. From c911ca1290965b31e530adff82da46aee2d568be Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 00:07:09 +0900 Subject: [PATCH 06/23] feat(erlang): create version-specific worker pools from db config --- binding/erlang/src/ip2region.app.src | 3 ++ binding/erlang/src/ip2region_sup.erl | 37 ++++++++++++++++------ binding/erlang/test/ip2region_sup_test.erl | 17 ++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 binding/erlang/test/ip2region_sup_test.erl diff --git a/binding/erlang/src/ip2region.app.src b/binding/erlang/src/ip2region.app.src index 4de369c..8619174 100644 --- a/binding/erlang/src/ip2region.app.src +++ b/binding/erlang/src/ip2region.app.src @@ -11,6 +11,9 @@ {poolargs, [ {size, 1}, {max_overflow, 5} + ]}, + {db, [ + {ipv4, "ip2region.xdb"} ]} ]}, {modules, []}, diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl index 6aa6695..e93d505 100644 --- a/binding/erlang/src/ip2region_sup.erl +++ b/binding/erlang/src/ip2region_sup.erl @@ -2,7 +2,7 @@ %% 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 top level supervisor. %% @end %%%------------------------------------------------------------------- @@ -18,7 +18,6 @@ start_link() -> {ok, SupPid} = supervisor:start_link({local, ?SERVER}, ?MODULE, []), - {ok, _PoolPid} = start_ip2region_pool(SupPid), {ok, SupPid}. %% sup_flags() = #{strategy => strategy(), % optional @@ -35,11 +34,11 @@ init([]) -> SupFlags = #{strategy => one_for_one, intensity => 10, period => 5}, - ChildSpecs = [], + ChildSpecs = pool_child_specs(), {ok, {SupFlags, ChildSpecs}}. %% internal functions -%% +%% create_table() -> Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], %% Legacy tables (kept for backward compatibility) @@ -60,10 +59,28 @@ ensure_table(Name, Opts) -> _ -> ok end. -start_ip2region_pool(Sup) -> +pool_child_specs() -> + {ok, DbConfig} = application:get_env(db), {ok, PoolArgsCfg} = application:get_env(poolargs), - PoolName = ?IP2REGION_POOL, - PoolArgs = [{strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg], - WorkerArgs = [], - ChildSpecs = poolboy:child_spec(PoolName, PoolArgs, WorkerArgs), - supervisor:start_child(Sup, ChildSpecs). \ No newline at end of file + Versions = [Version || {Version, _File} <- DbConfig], + UseLegacyName = (Versions == [ipv4]), + lists:foldl( + fun({ipv4, File}, Acc) when UseLegacyName -> + [make_pool_spec(?IP2REGION_POOL, ipv4, File, PoolArgsCfg) | Acc]; + ({ipv4, File}, Acc) -> + [make_pool_spec(?IP2REGION_POOL_V4, ipv4, File, PoolArgsCfg) | Acc]; + ({ipv6, File}, Acc) -> + [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}, {version, Version}], + poolboy:child_spec(PoolName, PoolArgs, WorkerArgs). diff --git a/binding/erlang/test/ip2region_sup_test.erl b/binding/erlang/test/ip2region_sup_test.erl new file mode 100644 index 0000000..d7a9c6f --- /dev/null +++ b/binding/erlang/test/ip2region_sup_test.erl @@ -0,0 +1,17 @@ +-module(ip2region_sup_test). +-include_lib("eunit/include/eunit.hrl"). +-include("ip2region.hrl"). + +pools_started_test() -> + application:stop(ip2region), + application:unload(ip2region), + ok = application:load(ip2region), + {ok, Cwd} = file:get_cwd(), + V6File = filename:join([Cwd, "..", "..", "data", "ip2region_v6.xdb"]), + ok = application:set_env(ip2region, db, [ + {ipv4, "ip2region.xdb"}, + {ipv6, V6File} + ]), + application:ensure_started(ip2region), + ?assert(is_pid(whereis(?IP2REGION_POOL_V4))), + ?assert(is_pid(whereis(?IP2REGION_POOL_V6))). From 8849cc9130aa1234447c2cdb96aa5bd5600ce978 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 00:07:22 +0900 Subject: [PATCH 07/23] feat(erlang): auto-route search by IPv4/IPv6 to correct worker pool --- binding/erlang/src/xdb.erl | 55 ++++++++++++++++++++------------ binding/erlang/test/xdb_test.erl | 17 +++++++++- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/binding/erlang/src/xdb.erl b/binding/erlang/src/xdb.erl index 2b98ac6..d7e7b4a 100644 --- a/binding/erlang/src/xdb.erl +++ b/binding/erlang/src/xdb.erl @@ -1,11 +1,10 @@ - %%%------------------------------------------------------------------- %% 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 client search api +%% +%% @doc +%% ip2region xdb client search api with IPv4/IPv6 auto-routing. %% @end %%%------------------------------------------------------------------- -module(xdb). @@ -13,20 +12,36 @@ -export([search/1]). --spec search(Ip :: tuple() | list() | binary()) -> Result :: binary | {error, Reason::atom()}. -search(Ip) when is_integer(Ip); is_list(Ip); is_tuple(Ip); is_binary(Ip) -> - case ip2region_util:ipv4_to_n(Ip) of - IntIp when is_integer(IntIp) -> - case ets:lookup(?IP2REGION_CACHE, IntIp) of - [{_IntIp, Region}] -> Region; - _ -> - Worker = poolboy:checkout(?IP2REGION_POOL, true, infinity), - try - ip2region_worker:search(Worker, IntIp) - after - poolboy:checkin(?IP2REGION_POOL, Worker) - end - end; - Ret -> - Ret +-spec search(Ip :: tuple() | list() | binary() | integer()) -> + Result :: list() | {error, Reason::atom()}. +search(Ip) -> + case ip2region_util:ip_to_bytes(Ip) of + {ok, ipv4, IpBin} -> + do_search(v4_pool(), ipv4, IpBin); + {ok, ipv6, IpBin} -> + do_search(?IP2REGION_POOL_V6, ipv6, IpBin); + Ret -> + Ret end. + +do_search(PoolName, Version, IpBin) -> + CacheTable = cache_table(Version), + case ets:lookup(CacheTable, IpBin) of + [{_, Region}] -> Region; + _ -> + Worker = poolboy:checkout(PoolName, true, infinity), + try + ip2region_worker:search(Worker, IpBin) + after + poolboy:checkin(PoolName, Worker) + end + end. + +v4_pool() -> + case whereis(?IP2REGION_POOL) of + Pid when is_pid(Pid) -> ?IP2REGION_POOL; + undefined -> ?IP2REGION_POOL_V4 + end. + +cache_table(ipv4) -> ?IP2REGION_CACHE_V4; +cache_table(ipv6) -> ?IP2REGION_CACHE_V6. diff --git a/binding/erlang/test/xdb_test.erl b/binding/erlang/test/xdb_test.erl index 85adcac..aea2ab2 100644 --- a/binding/erlang/test/xdb_test.erl +++ b/binding/erlang/test/xdb_test.erl @@ -4,7 +4,7 @@ search_test_() -> application:ensure_started(ip2region), - A = "中国|0|广东省|广州市|电信", + A = "中国|广东省|广州市|中国电信|CN", Region0 = xdb:search("1.0.8.0"), Region1 = xdb:search(<<"1.0.8.0">>), Region2 = xdb:search({1,0,8,0}), @@ -14,5 +14,20 @@ search_test_() -> ?_assert(A =:= Region1), ?_assert(A =:= Region2), ?_assert({error, bad_ip_format} =:= Region3) + ]. +ipv6_search_test_() -> + application:ensure_started(ip2region), + [ + ?_assert(is_list(xdb:search("2001:4860:4860::8888"))), + ?_assert(is_list(xdb:search(<<"2001:4860:4860::8888">>))), + ?_assert(is_list(xdb:search({8193, 10304, 10304, 0, 0, 0, 0, 34952}))) + ]. + +invalid_search_test_() -> + application:ensure_started(ip2region), + [ + ?_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})) ]. \ No newline at end of file From 06d54d8084bbda6399ff273398ec0fc17bbfdbba Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 00:31:17 +0900 Subject: [PATCH 08/23] docs(erlang): add dual-stack IPv4/IPv6 setup and usage guide --- binding/erlang/README.md | 53 ++++++++++++++++++++++++++++++------- binding/erlang/README_zh.md | 22 +++++++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 08b9999..99d99c3 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -15,10 +15,32 @@ The configurable parameters for this application are in `ip2region.app.src`, as {poolargs, [ {size, 1}, %% Default 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 ``` @@ -37,17 +59,28 @@ Call the `xdb:search/1` interface in the Erlang shell to query IP address inform ``` 1> xdb:search("1.0.8.0"). -[20013,22269,124,48,124,24191,19996,30465,124,24191,24030, - 24066,124,30005,20449] +[20013,22269,124,24191,19996,30465,124,24191,24030,24066, + 124,20013,22269,30005,20449,124,67,78] 2> 3> io:format("~ts~n", [xdb:search("1.0.8.0")]). -中国|0|广东省|广州市|电信 -io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]). -中国|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">>)]). +中国|广东省|广州市|中国电信|CN +5> io:format("~ts~n", [xdb:search({1,0,8,0})]). +中国|广东省|广州市|中国电信|CN 6> io:format("~ts~n", [xdb:search(16779264)]). -中国|0|广东省|广州市|电信 +中国|广东省|广州市|中国电信|CN +``` + +With dual-stack enabled, IPv6 addresses are supported in the same way: + +``` +1> io:format("~ts~n", [xdb:search("2001:4860:4860::8888")]). +... +2> io:format("~ts~n", [xdb:search(<<"2001:4860:4860::8888">>)]). +... +3> io:format("~ts~n", [xdb:search({8193,10304,10304,0,0,0,0,34952})]). +... ``` ### Usage @@ -92,8 +125,8 @@ $ rebar3 eunit XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb .... -Finished in 0.074 seconds -4 tests, 0 failures +Finished in 0.150 seconds +34 tests, 0 failures ``` ### Benchmark diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index 0ebc6d6..6643b07 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -12,10 +12,32 @@ {poolargs, [ {size, 1}, %% 工作进程默认数量 {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 输入,并将其路由到对应的工作进程池。 + ### 编译 ``` From d189535bb8f6e735f821755b2dc36a479033cdbc Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 00:51:53 +0900 Subject: [PATCH 09/23] chore(erlang): add Makefile bench targets, IPv6-capable benchmark script and docs --- binding/erlang/Makefile | 18 +++++ binding/erlang/README.md | 84 ++++++++++++++------- binding/erlang/README_zh.md | 85 +++++++++++++++------- binding/erlang/benchmarks/xdb-benchmark.sh | 35 ++++++++- binding/erlang/src/xdb_benchmark.erl | 19 +++-- 5 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 binding/erlang/Makefile diff --git a/binding/erlang/Makefile b/binding/erlang/Makefile new file mode 100644 index 0000000..d71c6d4 --- /dev/null +++ b/binding/erlang/Makefile @@ -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 diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 99d99c3..5a51647 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -131,44 +131,78 @@ Finished in 0.150 seconds ### Benchmark +Both IPv4 and IPv6 benchmarks share the same script. Run it with the desired IP version: + ``` $ cd benchmarks/ -$ sh xdb-benchmark.sh -===> Verifying dependencies... -===> Analyzing applications... -===> Compiling ip2region -Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit] +$ sh xdb-benchmark.sh ipv4 +``` -Eshell V12.3.2.2 (abort with ^G) -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 +For IPv6: -===> Booted ip2region -===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()." +``` +$ sh xdb-benchmark.sh ipv6 +``` + +Or use the Makefile targets from the `binding/erlang` directory: + +``` +$ make bench-v4 +$ make bench-v6 +``` + +#### IPv4 benchmark example + +``` CPU info: -model name : AMD EPYC 7K62 48-Core Processor -cache size : 512 KB -cpu MHz : 2595.124 -bogomips : 5190.24 -cores/threads : 2 +model name : Apple M4 +cores/threads : 10/10 Erlang info: -system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit] -load test data use 4.835593s +system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] +load test data use 1.34496s start run benchmark tests search from file: -ip count:683844, -total time: 28.201699s, -search 24248.326315375536 times per second, -use 41.23995969841075 micro second per search +ip count:487169, +total time: 11.564375s, +search 42126.70377776577 times per second, +use 23.737912305585947 micro second per search search from cache: -ip count:683844, -total time: 0.671801s, -search 1017926.4395259906 times per second, -use 0.9823892583688677 micro second per search +ip count:487169, +total time: 0.213552s, +search 2281266.389450813 times per second, +use 0.43835301507279817 micro second per search + +benchmark test finish +``` + +#### IPv6 benchmark example + +``` +CPU info: +model name : Apple M4 +cores/threads : 10/10 + +Erlang info: +system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] +load test data use 2.472659s + +start run benchmark tests + +search from file: +ip count:638953, +total time: 18.463896s, +search 34605.535039842085 times per second, +use 28.897111368128797 micro second per search + +search from cache: +ip count:638953, +total time: 0.541899s, +search 1179099.7953493178 times per second, +use 0.8481046336741513 micro second per search benchmark test finish ``` diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index 6643b07..4355a9a 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -107,44 +107,79 @@ Finished in 0.074 seconds ``` ### 基准测试 + +IPv4 与 IPv6 共用一个脚本,通过参数指定版本: + ``` $ cd benchmarks/ -$ sh xdb-benchmark.sh -===> Verifying dependencies... -===> Analyzing applications... -===> Compiling ip2region -Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit] +$ sh xdb-benchmark.sh ipv4 +``` -Eshell V12.3.2.2 (abort with ^G) -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 +IPv6: -===> Booted ip2region -===> Evaluating: "xdb_benchmark:main(\"../../data/ip.merge.txt\"), init:stop()." +``` +$ sh xdb-benchmark.sh ipv6 +``` + +也可以在 `binding/erlang` 目录直接用 Makefile: + +``` +$ make bench-v4 +$ make bench-v6 +``` + +#### IPv4 基准测试示例 + +``` CPU info: -model name : AMD EPYC 7K62 48-Core Processor -cache size : 512 KB -cpu MHz : 2595.124 -bogomips : 5190.24 -cores/threads : 2 +model name : Apple M4 +cores/threads : 10/10 Erlang info: -system_version:Erlang/OTP 24 [erts-12.3.2.2] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit] -load test data use 4.835593s +system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] +load test data use 1.34496s start run benchmark tests search from file: -ip count:683844, -total time: 28.201699s, -search 24248.326315375536 times per second, -use 41.23995969841075 micro second per search +ip count:487169, +total time: 11.564375s, +search 42126.70377776577 times per second, +use 23.737912305585947 micro second per search search from cache: -ip count:683844, -total time: 0.671801s, -search 1017926.4395259906 times per second, -use 0.9823892583688677 micro second per search +ip count:487169, +total time: 0.213552s, +search 2281266.389450813 times per second, +use 0.43835301507279817 micro second per search + +benchmark test finish +``` + +#### IPv6 基准测试示例 + +``` +CPU info: +model name : Apple M4 +cores/threads : 10/10 + +Erlang info: +system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] +load test data use 2.472659s + +start run benchmark tests + +search from file: +ip count:638953, +total time: 18.463896s, +search 34605.535039842085 times per second, +use 28.897111368128797 micro second per search + +search from cache: +ip count:638953, +total time: 0.541899s, +search 1179099.7953493178 times per second, +use 0.8481046336741513 micro second per search benchmark test finish ``` diff --git a/binding/erlang/benchmarks/xdb-benchmark.sh b/binding/erlang/benchmarks/xdb-benchmark.sh index 40d13f3..218cdf4 100755 --- a/binding/erlang/benchmarks/xdb-benchmark.sh +++ b/binding/erlang/benchmarks/xdb-benchmark.sh @@ -1,5 +1,36 @@ #!/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="[{ipv4, \"ip2region.xdb\"}, {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 \ + -noshell \ + -eval "${EVAL}" diff --git a/binding/erlang/src/xdb_benchmark.erl b/binding/erlang/src/xdb_benchmark.erl index e52801c..0fa6e7f 100644 --- a/binding/erlang/src/xdb_benchmark.erl +++ b/binding/erlang/src/xdb_benchmark.erl @@ -18,11 +18,20 @@ main(DataFile) -> show_hw_sw_info() -> io:format("CPU info:~n", []), - io:format("~s", [os:cmd("egrep '^model name' /proc/cpuinfo | head -1")]), - io:format("~s", [os:cmd("egrep '^cache' /proc/cpuinfo | head -1")]), - io:format("~s", [os:cmd("egrep '^cpu MHz' /proc/cpuinfo | head -1")]), - io:format("~s", [os:cmd("egrep '^bogomips' /proc/cpuinfo | head -1")]), - io:format("cores/threads : ~s~n", [os:cmd("egrep -c '^processor' /proc/cpuinfo")]), + case os:type() of + {unix, darwin} -> + io:format("model name : ~s~n", [string:trim(os:cmd("sysctl -n machdep.cpu.brand_string 2>/dev/null"))]), + io:format("cores/threads : ~s/~s~n", [string:trim(os:cmd("sysctl -n hw.physicalcpu 2>/dev/null")), + string:trim(os:cmd("sysctl -n hw.logicalcpu 2>/dev/null"))]); + {unix, linux} -> + io:format("~s", [os:cmd("egrep '^model name' /proc/cpuinfo | head -1")]), + io:format("~s", [os:cmd("egrep '^cache' /proc/cpuinfo | head -1")]), + io:format("~s", [os:cmd("egrep '^cpu MHz' /proc/cpuinfo | head -1")]), + io:format("~s", [os:cmd("egrep '^bogomips' /proc/cpuinfo | head -1")]), + io:format("cores/threads : ~s~n", [os:cmd("egrep -c '^processor' /proc/cpuinfo")]); + _ -> + io:format("unsupported os~n", []) + end, io:format("Erlang info:~n", []), io:format("system_version:~s", [erlang:system_info(system_version)]), ok. From a1667d118239d611ded6d83bc9c918a2a5dbb800 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 01:04:00 +0900 Subject: [PATCH 10/23] refactor(erlang): simplify benchmark output to ms/QPS and add CPU clock info --- binding/erlang/README.md | 60 ++++-------- binding/erlang/README_zh.md | 60 ++++-------- binding/erlang/benchmarks/xdb-benchmark.sh | 1 + binding/erlang/src/xdb_benchmark.erl | 102 +++++++++++++++------ 4 files changed, 112 insertions(+), 111 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 5a51647..73eeb6b 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -154,55 +154,31 @@ $ make bench-v6 #### IPv4 benchmark example ``` -CPU info: -model name : Apple M4 -cores/threads : 10/10 +System: + CPU : Apple M4 + 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 : 487169 IPs in 1.574 s -Erlang info: -system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] -load test data use 1.34496s +Benchmarks: + file total= 9.460s count= 487169 qps= 51500.40 avg= 0.019417 ms/op (19.417 us/op) + cache total= 0.150s count= 487169 qps= 3245240.41 avg= 0.000308 ms/op ( 0.308 us/op) -start run benchmark tests - -search from file: -ip count:487169, -total time: 11.564375s, -search 42126.70377776577 times per second, -use 23.737912305585947 micro second per search - -search from cache: -ip count:487169, -total time: 0.213552s, -search 2281266.389450813 times per second, -use 0.43835301507279817 micro second per search - -benchmark test finish +Done. ``` #### IPv6 benchmark example ``` -CPU info: -model name : Apple M4 -cores/threads : 10/10 +System: + CPU : Apple M4 + 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.724 s -Erlang info: -system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] -load test data use 2.472659s +Benchmarks: + file total= 16.326s count= 638953 qps= 39137.65 avg= 0.025551 ms/op (25.551 us/op) + cache total= 0.469s count= 638953 qps= 1361972.38 avg= 0.000734 ms/op ( 0.734 us/op) -start run benchmark tests - -search from file: -ip count:638953, -total time: 18.463896s, -search 34605.535039842085 times per second, -use 28.897111368128797 micro second per search - -search from cache: -ip count:638953, -total time: 0.541899s, -search 1179099.7953493178 times per second, -use 0.8481046336741513 micro second per search - -benchmark test finish +Done. ``` diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index 4355a9a..a6d3022 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -131,55 +131,31 @@ $ make bench-v6 #### IPv4 基准测试示例 ``` -CPU info: -model name : Apple M4 -cores/threads : 10/10 +System: + CPU : Apple M4 + 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 : 487169 IPs in 1.574 s -Erlang info: -system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] -load test data use 1.34496s +Benchmarks: + file total= 9.460s count= 487169 qps= 51500.40 avg= 0.019417 ms/op (19.417 us/op) + cache total= 0.150s count= 487169 qps= 3245240.41 avg= 0.000308 ms/op ( 0.308 us/op) -start run benchmark tests - -search from file: -ip count:487169, -total time: 11.564375s, -search 42126.70377776577 times per second, -use 23.737912305585947 micro second per search - -search from cache: -ip count:487169, -total time: 0.213552s, -search 2281266.389450813 times per second, -use 0.43835301507279817 micro second per search - -benchmark test finish +Done. ``` #### IPv6 基准测试示例 ``` -CPU info: -model name : Apple M4 -cores/threads : 10/10 +System: + CPU : Apple M4 + 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.724 s -Erlang info: -system_version:Erlang/OTP 29 [erts-17.0.2] [source] [64-bit] [smp:10:10] [ds:10:10:10] [async-threads:1] [jit] [dtrace] -load test data use 2.472659s +Benchmarks: + file total= 16.326s count= 638953 qps= 39137.65 avg= 0.025551 ms/op (25.551 us/op) + cache total= 0.469s count= 638953 qps= 1361972.38 avg= 0.000734 ms/op ( 0.734 us/op) -start run benchmark tests - -search from file: -ip count:638953, -total time: 18.463896s, -search 34605.535039842085 times per second, -use 28.897111368128797 micro second per search - -search from cache: -ip count:638953, -total time: 0.541899s, -search 1179099.7953493178 times per second, -use 0.8481046336741513 micro second per search - -benchmark test finish +Done. ``` diff --git a/binding/erlang/benchmarks/xdb-benchmark.sh b/binding/erlang/benchmarks/xdb-benchmark.sh index 218cdf4..e8aebc3 100755 --- a/binding/erlang/benchmarks/xdb-benchmark.sh +++ b/binding/erlang/benchmarks/xdb-benchmark.sh @@ -32,5 +32,6 @@ 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}" diff --git a/binding/erlang/src/xdb_benchmark.erl b/binding/erlang/src/xdb_benchmark.erl index 0fa6e7f..e5e5ad1 100644 --- a/binding/erlang/src/xdb_benchmark.erl +++ b/binding/erlang/src/xdb_benchmark.erl @@ -11,30 +11,76 @@ -export([main/1]). 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), + application:ensure_started(ip2region), show_hw_sw_info(), IpList = load_test_data(DataFile), run(IpList). show_hw_sw_info() -> - io:format("CPU info:~n", []), + {Model, Clock, Cores, Threads} = cpu_info(), + io:format("~nSystem:~n", []), + io:format(" CPU : ~s", [Model]), + case Clock of + "" -> io:format("~n", []); + _ -> io:format(" @ ~s~n", [Clock]) + end, + io:format(" Cores : ~s cores / ~s threads~n", [Cores, Threads]), + io:format(" Erlang : ~s~n", [string:trim(erlang:system_info(system_version))]), + ok. + +cpu_info() -> case os:type() of {unix, darwin} -> - io:format("model name : ~s~n", [string:trim(os:cmd("sysctl -n machdep.cpu.brand_string 2>/dev/null"))]), - io:format("cores/threads : ~s/~s~n", [string:trim(os:cmd("sysctl -n hw.physicalcpu 2>/dev/null")), - string:trim(os:cmd("sysctl -n hw.logicalcpu 2>/dev/null"))]); + 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} -> - io:format("~s", [os:cmd("egrep '^model name' /proc/cpuinfo | head -1")]), - io:format("~s", [os:cmd("egrep '^cache' /proc/cpuinfo | head -1")]), - io:format("~s", [os:cmd("egrep '^cpu MHz' /proc/cpuinfo | head -1")]), - io:format("~s", [os:cmd("egrep '^bogomips' /proc/cpuinfo | head -1")]), - io:format("cores/threads : ~s~n", [os:cmd("egrep -c '^processor' /proc/cpuinfo")]); + Model = linux_cpu_field("model name"), + Clock = format_clock_mhz(linux_cpu_field("cpu MHz")), + Cores = trim(os:cmd("grep -c '^processor' /proc/cpuinfo 2>/dev/null")), + Threads = Cores, + {Model, Clock, Cores, Threads}; _ -> - io:format("unsupported os~n", []) - end, - io:format("Erlang info:~n", []), - io:format("system_version:~s", [erlang:system_info(system_version)]), - ok. + {"unknown", "", "?", "?"} + end. + +sysctl(Key) -> + 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/^ *//'", + 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(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 GHz", [Hz / 1000000000])); + _ -> + "" + end. + +format_clock_mhz(MhzStr) -> + case string:to_float(trim(MhzStr)) of + {ok, Mhz, _} -> + lists:flatten(io_lib:format("~.3f GHz", [Mhz / 1000])); + _ -> + "" + end. load_test_data(DataFile) -> {ok, Fd} = file:open(DataFile, [read]), @@ -42,7 +88,7 @@ load_test_data(DataFile) -> IpList = load_test_data(Fd, []), T1 = os:timestamp(), 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. load_test_data(Fd, IpList) -> @@ -55,30 +101,32 @@ load_test_data(Fd, IpList) -> load_test_data(Fd, IpList) end; _ -> - file:close(Fd), + file:close(Fd), IpList end. run(IpList) -> garbage_collect(), - io:format("~nstart run benchmark tests~n", []), - io:format("~nsearch from file:~n", []), - run_test(IpList), - io:format("~nsearch from cache:~n", []), - run_test(IpList), - io:format("~nbenchmark test finish~n", []). + io:format("~nBenchmarks:~n", []), + run_test("file", IpList), + run_test("cache", IpList), + io:format("~nDone.~n", []). -run_test(IpList) -> +run_test(Label, IpList) -> T0 = os:timestamp(), run_test_aux(IpList), T1 = os:timestamp(), Sec = timer:now_diff(T1, T0) / 1000000, - IpCount = length(IpList), - io:format("ip count:~p,~ntotal time: ~ps,~nsearch ~p times per second,~nuse ~p micro second per search~n", - [IpCount, Sec, IpCount / Sec, Sec * 1000000/IpCount]). + Count = length(IpList), + Qps = Count / Sec, + 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([Ip | Tail]) -> xdb:search(Ip), run_test_aux(Tail). +trim(Str) -> string:trim(Str). From 445eb4cb8eeede71dd539063af731d31c84435a9 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 01:50:39 +0900 Subject: [PATCH 11/23] fix(erlang): integer guard, cache dedup, dead macro cleanup --- binding/erlang/README.md | 12 ++-- binding/erlang/README_zh.md | 12 ++-- binding/erlang/benchmarks/xdb-benchmark.sh | 2 +- binding/erlang/include/ip2region.hrl | 18 +----- binding/erlang/src/ip2region_sup.erl | 15 +++-- binding/erlang/src/ip2region_util.erl | 6 +- binding/erlang/src/ip2region_worker.erl | 69 ++++++++------------- binding/erlang/src/ip2region_xdb.erl | 10 +++ binding/erlang/src/xdb.erl | 17 +++-- binding/erlang/src/xdb_benchmark.erl | 18 +++--- binding/erlang/test/ip2region_sup_test.erl | 7 ++- binding/erlang/test/ip2region_util_test.erl | 6 +- 12 files changed, 86 insertions(+), 106 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 73eeb6b..7bb9e2b 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -158,11 +158,11 @@ System: CPU : Apple M4 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 : 487169 IPs in 1.574 s + Loaded : 487169 IPs in 1.335 s Benchmarks: - file total= 9.460s count= 487169 qps= 51500.40 avg= 0.019417 ms/op (19.417 us/op) - cache total= 0.150s count= 487169 qps= 3245240.41 avg= 0.000308 ms/op ( 0.308 us/op) + cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op) + warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op) Done. ``` @@ -174,11 +174,11 @@ System: CPU : Apple M4 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.724 s + Loaded : 638953 IPs in 2.949 s Benchmarks: - file total= 16.326s count= 638953 qps= 39137.65 avg= 0.025551 ms/op (25.551 us/op) - cache total= 0.469s count= 638953 qps= 1361972.38 avg= 0.000734 ms/op ( 0.734 us/op) + 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. ``` diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index a6d3022..89d3d72 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -135,11 +135,11 @@ System: CPU : Apple M4 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 : 487169 IPs in 1.574 s + Loaded : 487169 IPs in 1.335 s Benchmarks: - file total= 9.460s count= 487169 qps= 51500.40 avg= 0.019417 ms/op (19.417 us/op) - cache total= 0.150s count= 487169 qps= 3245240.41 avg= 0.000308 ms/op ( 0.308 us/op) + cold total= 9.601s count= 487169 qps= 50740.66 avg= 0.019708 ms/op (19.708 us/op) + warm total= 0.160s count= 487169 qps= 3053164.29 avg= 0.000328 ms/op ( 0.328 us/op) Done. ``` @@ -151,11 +151,11 @@ System: CPU : Apple M4 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.724 s + Loaded : 638953 IPs in 2.949 s Benchmarks: - file total= 16.326s count= 638953 qps= 39137.65 avg= 0.025551 ms/op (25.551 us/op) - cache total= 0.469s count= 638953 qps= 1361972.38 avg= 0.000734 ms/op ( 0.734 us/op) + 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. ``` diff --git a/binding/erlang/benchmarks/xdb-benchmark.sh b/binding/erlang/benchmarks/xdb-benchmark.sh index e8aebc3..77b647b 100755 --- a/binding/erlang/benchmarks/xdb-benchmark.sh +++ b/binding/erlang/benchmarks/xdb-benchmark.sh @@ -10,7 +10,7 @@ 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="[{ipv4, \"ip2region.xdb\"}, {ipv6, \"${DB_FILE}\"}]" + DB_CONFIG="[{ipv6, \"${DB_FILE}\"}]" elif [ "$VERSION" = "ipv4" ]; then DATA_FILE="${PROJECT_ROOT}/data/ipv4_source.txt" DB_CONFIG="[{ipv4, \"ip2region.xdb\"}]" diff --git a/binding/erlang/include/ip2region.hrl b/binding/erlang/include/ip2region.hrl index 2e7ea80..1cf474f 100644 --- a/binding/erlang/include/ip2region.hrl +++ b/binding/erlang/include/ip2region.hrl @@ -4,20 +4,11 @@ -define(NONE, none). -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_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_SEGMENT_INDEX_SIZE, 14). - --define(IP2REGION_POOL, ip2region_pool). - %% xdb header field offsets (bytes) -define(XDB_HEADER_VERSION_OFFSET, 0). -define(XDB_HEADER_INDEX_POLICY_OFFSET, 2). @@ -26,7 +17,6 @@ -define(XDB_HEADER_END_INDEX_PTR_OFFSET, 12). -define(XDB_HEADER_IP_VERSION_OFFSET, 16). -define(XDB_HEADER_RUNTIME_PTR_BYTES_OFFSET, 18). --define(XDB_HEADER_INFO_LENGTH, 256). %% IP versions as stored in xdb header -define(IP_VERSION_4, 4). @@ -45,12 +35,8 @@ -define(IP2REGION_CACHE_V6, ets_ip2region_cache_v6). %% Per-version pool names. ?IP2REGION_POOL is kept as the legacy v4 pool name. +-define(IP2REGION_POOL, ip2region_pool). -define(IP2REGION_POOL_V4, ip2region_pool_v4). -define(IP2REGION_POOL_V6, ip2region_pool_v6). --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. \ No newline at end of file diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl index e93d505..a111589 100644 --- a/binding/erlang/src/ip2region_sup.erl +++ b/binding/erlang/src/ip2region_sup.erl @@ -41,10 +41,6 @@ init([]) -> %% create_table() -> Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], - %% Legacy tables (kept for backward compatibility) - ensure_table(?XDB_VECTOR_INDEX, Opts), - ensure_table(?XDB_SEGMENT_INDEX, Opts), - ensure_table(?IP2REGION_CACHE, Opts), %% Version-specific tables for dual-stack support ensure_table(?XDB_VECTOR_INDEX_V4, Opts), ensure_table(?XDB_VECTOR_INDEX_V6, Opts), @@ -64,11 +60,14 @@ pool_child_specs() -> {ok, PoolArgsCfg} = application:get_env(poolargs), Versions = [Version || {Version, _File} <- DbConfig], UseLegacyName = (Versions == [ipv4]), + V4PoolName = case UseLegacyName of + true -> ?IP2REGION_POOL; + false -> ?IP2REGION_POOL_V4 + end, + ok = application:set_env(?APP_NAME, v4_pool_name, V4PoolName), lists:foldl( - fun({ipv4, File}, Acc) when UseLegacyName -> - [make_pool_spec(?IP2REGION_POOL, ipv4, File, PoolArgsCfg) | Acc]; - ({ipv4, File}, Acc) -> - [make_pool_spec(?IP2REGION_POOL_V4, ipv4, File, PoolArgsCfg) | Acc]; + fun({ipv4, File}, Acc) -> + [make_pool_spec(V4PoolName, ipv4, File, PoolArgsCfg) | Acc]; ({ipv6, File}, Acc) -> [make_pool_spec(?IP2REGION_POOL_V6, ipv6, File, PoolArgsCfg) | Acc]; (_, Acc) -> diff --git a/binding/erlang/src/ip2region_util.erl b/binding/erlang/src/ip2region_util.erl index df892f9..db5ec46 100644 --- a/binding/erlang/src/ip2region_util.erl +++ b/binding/erlang/src/ip2region_util.erl @@ -13,7 +13,7 @@ -spec ip_version(Ip :: tuple() | list() | binary() | integer()) -> ipv4 | ipv6 | {error, atom()}. -ip_version(Ip) when is_integer(Ip) -> ipv4; +ip_version(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF -> ipv4; ip_version({_, _, _, _}) -> ipv4; ip_version({_, _, _, _, _, _, _, _}) -> ipv6; ip_version(Ip) when is_binary(Ip) -> @@ -29,7 +29,7 @@ ip_version(_) -> -spec ip_to_bytes(Ip :: tuple() | list() | binary() | integer()) -> {ok, ipv4 | ipv6, binary()} | {error, atom()}. -ip_to_bytes(Ip) when is_integer(Ip) -> +ip_to_bytes(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF -> {ok, ipv4, <>}; ip_to_bytes({A, B, C, D}) -> {ok, ipv4, <>}; @@ -51,7 +51,7 @@ ip_to_bytes(_) -> -spec ipv4_to_n(Ip :: tuple() | list() | binary() | integer()) -> non_neg_integer() | {error, atom()}. -ipv4_to_n(IntIp) when is_integer(IntIp) -> IntIp; +ipv4_to_n(IntIp) when is_integer(IntIp), IntIp >= 0, IntIp =< 16#FFFFFFFF -> IntIp; ipv4_to_n({A, B, C, D}) -> <> = <>, N; diff --git a/binding/erlang/src/ip2region_worker.erl b/binding/erlang/src/ip2region_worker.erl index e207635..216fd76 100644 --- a/binding/erlang/src/ip2region_worker.erl +++ b/binding/erlang/src/ip2region_worker.erl @@ -18,8 +18,7 @@ -record(state, { xdb_fd, - version :: ipv4 | ipv6, - segment_index_size :: pos_integer() + version :: ipv4 | ipv6 }). %%========================================== @@ -63,14 +62,14 @@ init(Args) -> {ok, IoDevice} = file:open(XdbFileName, [read, binary]), {ok, HeaderBin} = file:read(IoDevice, ?XDB_HEADER_SIZE), {ok, Header} = ip2region_xdb:parse_header(HeaderBin), - Version = resolve_version(Header), - SegmentIndexSize = ip2region_xdb:segment_index_size(Version), - load_vector_index(IoDevice, Version), - {ok, #state{ - xdb_fd = IoDevice, - version = Version, - segment_index_size = SegmentIndexSize - }}. + case resolve_version(Header) of + {ok, Version} -> + load_vector_index(IoDevice, Version), + {ok, #state{xdb_fd = IoDevice, version = Version}}; + {error, Reason} -> + file:close(IoDevice), + {stop, Reason} + end. handle_call(Request, From, State) -> try @@ -137,18 +136,18 @@ do_info(Info, State) -> resolve_version(Header) -> case ip2region_xdb:header_version(Header) of - 2 -> ipv4; + 2 -> {ok, ipv4}; 3 -> case ip2region_xdb:header_ip_version(Header) of - ?IP_VERSION_4 -> ipv4; - ?IP_VERSION_6 -> ipv6; - _ -> ipv4 + ?IP_VERSION_4 -> {ok, ipv4}; + ?IP_VERSION_6 -> {ok, ipv6}; + V -> {error, {invalid_xdb_ip_version, V}} end; - _ -> ipv4 + V -> {error, {invalid_xdb_version, V}} end. load_vector_index(IoDevice, Version) -> - Table = vector_index_table(Version), + Table = ip2region_xdb:vector_index_table(Version), case ets:info(Table, size) of undefined -> Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], @@ -162,7 +161,7 @@ load_vector_index(IoDevice, Version) -> load_vector_index_data(IoDevice, Table) -> {ok, VectorIndexBin} = - file:read(IoDevice, ?XDB_VECTOR_INDEX_COUNT * 8), + 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; @@ -170,24 +169,15 @@ load_vector_index_aux(<>, ets:insert(Table, {Index, SPtr, EPtr}), load_vector_index_aux(VectorIndexBin, Index + 1, Table). -search_ip(IoDevice, IpInt, State) when is_integer(IpInt) -> - search_ip(IoDevice, <>, State); -search_ip(IoDevice, Ip, #state{version = Version, segment_index_size = SegSize}) -> - CacheTable = cache_table(Version), - VectorTable = vector_index_table(Version), - SegmentTable = segment_index_table(Version), - case ets:lookup(CacheTable, Ip) of - [{_, RegionInfo}] -> - RegionInfo; - _ -> - <> = Ip, - VectorIdx = A * ?XDB_VECTOR_COLS + B, - [{_, SPtr, EPtr}] = ets:lookup(VectorTable, VectorIdx), - RegionInfo = search_ip(IoDevice, Ip, SPtr, EPtr, 0, - (EPtr - SPtr) div SegSize, SegSize, Version, SegmentTable), - ets:insert_new(CacheTable, {Ip, RegionInfo}), - RegionInfo - end. +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), + <> = 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, @@ -240,12 +230,3 @@ decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V4) -> decode_segment_index(Bin, ?XDB_SEGMENT_INDEX_SIZE_V6) -> <> = Bin, {SIp, EIp, DataLen, DataPtr}. - -vector_index_table(ipv4) -> ?XDB_VECTOR_INDEX_V4; -vector_index_table(ipv6) -> ?XDB_VECTOR_INDEX_V6. - -segment_index_table(ipv4) -> ?XDB_SEGMENT_INDEX_V4; -segment_index_table(ipv6) -> ?XDB_SEGMENT_INDEX_V6. - -cache_table(ipv4) -> ?IP2REGION_CACHE_V4; -cache_table(ipv6) -> ?IP2REGION_CACHE_V6. diff --git a/binding/erlang/src/ip2region_xdb.erl b/binding/erlang/src/ip2region_xdb.erl index fc0f14e..eea6999 100644 --- a/binding/erlang/src/ip2region_xdb.erl +++ b/binding/erlang/src/ip2region_xdb.erl @@ -14,6 +14,7 @@ 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"). @@ -55,3 +56,12 @@ 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. + +vector_index_table(ipv4) -> ?XDB_VECTOR_INDEX_V4; +vector_index_table(ipv6) -> ?XDB_VECTOR_INDEX_V6. + +segment_index_table(ipv4) -> ?XDB_SEGMENT_INDEX_V4; +segment_index_table(ipv6) -> ?XDB_SEGMENT_INDEX_V6. + +cache_table(ipv4) -> ?IP2REGION_CACHE_V4; +cache_table(ipv6) -> ?IP2REGION_CACHE_V6. diff --git a/binding/erlang/src/xdb.erl b/binding/erlang/src/xdb.erl index d7e7b4a..d160382 100644 --- a/binding/erlang/src/xdb.erl +++ b/binding/erlang/src/xdb.erl @@ -13,7 +13,7 @@ -export([search/1]). -spec search(Ip :: tuple() | list() | binary() | integer()) -> - Result :: list() | {error, Reason::atom()}. + Result :: list() | {error, term()}. search(Ip) -> case ip2region_util:ip_to_bytes(Ip) of {ok, ipv4, IpBin} -> @@ -25,23 +25,22 @@ search(Ip) -> end. do_search(PoolName, Version, IpBin) -> - CacheTable = cache_table(Version), + CacheTable = ip2region_xdb:cache_table(Version), case ets:lookup(CacheTable, IpBin) of [{_, Region}] -> Region; _ -> Worker = poolboy:checkout(PoolName, true, infinity), try - ip2region_worker:search(Worker, IpBin) + Region = ip2region_worker:search(Worker, IpBin), + ets:insert(CacheTable, {IpBin, Region}), + Region after poolboy:checkin(PoolName, Worker) end end. v4_pool() -> - case whereis(?IP2REGION_POOL) of - Pid when is_pid(Pid) -> ?IP2REGION_POOL; - undefined -> ?IP2REGION_POOL_V4 + case application:get_env(?APP_NAME, v4_pool_name) of + {ok, PoolName} -> PoolName; + undefined -> ?IP2REGION_POOL end. - -cache_table(ipv4) -> ?IP2REGION_CACHE_V4; -cache_table(ipv6) -> ?IP2REGION_CACHE_V6. diff --git a/binding/erlang/src/xdb_benchmark.erl b/binding/erlang/src/xdb_benchmark.erl index e5e5ad1..0aa22db 100644 --- a/binding/erlang/src/xdb_benchmark.erl +++ b/binding/erlang/src/xdb_benchmark.erl @@ -46,7 +46,7 @@ cpu_info() -> {unix, linux} -> Model = linux_cpu_field("model name"), Clock = format_clock_mhz(linux_cpu_field("cpu MHz")), - Cores = trim(os:cmd("grep -c '^processor' /proc/cpuinfo 2>/dev/null")), + Cores = string:trim(os:cmd("grep -c '^processor' /proc/cpuinfo 2>/dev/null")), Threads = Cores, {Model, Clock, Cores, Threads}; _ -> @@ -54,28 +54,28 @@ cpu_info() -> end. sysctl(Key) -> - trim(os:cmd("sysctl -n " ++ Key ++ " 2>/dev/null")). + 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/^ *//'", - trim(os:cmd(Cmd)). + 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(trim(HzStr)) of + 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 GHz", [Hz / 1000000000])); + lists:flatten(io_lib:format("~.2f MHz", [Hz / 1000000])); _ -> "" end. format_clock_mhz(MhzStr) -> - case string:to_float(trim(MhzStr)) of + case string:to_float(string:trim(MhzStr)) of {ok, Mhz, _} -> lists:flatten(io_lib:format("~.3f GHz", [Mhz / 1000])); _ -> @@ -108,8 +108,8 @@ load_test_data(Fd, IpList) -> run(IpList) -> garbage_collect(), io:format("~nBenchmarks:~n", []), - run_test("file", IpList), - run_test("cache", IpList), + run_test("cold", IpList), + run_test("warm", IpList), io:format("~nDone.~n", []). run_test(Label, IpList) -> @@ -128,5 +128,3 @@ run_test_aux([]) -> ok; run_test_aux([Ip | Tail]) -> xdb:search(Ip), run_test_aux(Tail). - -trim(Str) -> string:trim(Str). diff --git a/binding/erlang/test/ip2region_sup_test.erl b/binding/erlang/test/ip2region_sup_test.erl index d7a9c6f..3ed6162 100644 --- a/binding/erlang/test/ip2region_sup_test.erl +++ b/binding/erlang/test/ip2region_sup_test.erl @@ -6,8 +6,11 @@ pools_started_test() -> application:stop(ip2region), application:unload(ip2region), ok = application:load(ip2region), - {ok, Cwd} = file:get_cwd(), - V6File = filename:join([Cwd, "..", "..", "data", "ip2region_v6.xdb"]), + TestDir = filename:dirname(?FILE), + ErlangDir = filename:dirname(TestDir), + BindingDir = filename:dirname(ErlangDir), + RepoRoot = filename:dirname(BindingDir), + V6File = filename:join([RepoRoot, "data", "ip2region_v6.xdb"]), ok = application:set_env(ip2region, db, [ {ipv4, "ip2region.xdb"}, {ipv6, V6File} diff --git a/binding/erlang/test/ip2region_util_test.erl b/binding/erlang/test/ip2region_util_test.erl index 200f0a0..ff8d0f2 100644 --- a/binding/erlang/test/ip2region_util_test.erl +++ b/binding/erlang/test/ip2region_util_test.erl @@ -30,7 +30,11 @@ 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(atom)) + ?_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:ipv4_to_n(-1)), + ?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n(16#100000000)) ]. legacy_ipv4_to_n_test_() -> From 25513cb2468fc93f3c3192fe8863fa83536e58fa Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 03:45:13 +0900 Subject: [PATCH 12/23] fix(erlang): unify v4 pool name and stop caching errors --- binding/erlang/README.md | 4 ++++ binding/erlang/README_zh.md | 3 +++ binding/erlang/include/ip2region.hrl | 22 +++++++++++----------- binding/erlang/src/ip2region_sup.erl | 9 +-------- binding/erlang/src/ip2region_worker.erl | 5 +++++ binding/erlang/src/ip2region_xdb.erl | 13 +++---------- binding/erlang/src/xdb.erl | 17 +++++++---------- binding/erlang/test/ip2region_sup_test.erl | 4 ++++ 8 files changed, 38 insertions(+), 39 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 7bb9e2b..29f363a 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -133,6 +133,10 @@ Finished in 0.150 seconds 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. + ``` $ cd benchmarks/ $ sh xdb-benchmark.sh ipv4 diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index 89d3d72..d482169 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -110,6 +110,9 @@ Finished in 0.074 seconds IPv4 与 IPv6 共用一个脚本,通过参数指定版本: +> `cold` = 第一次遍历源文件:每个 IP 都会触发真实查询,并把结果写入 ETS 缓存。 +> `warm` = 第二次遍历同一列表,所有查询都直接命中 ETS 缓存。 + ``` $ cd benchmarks/ $ sh xdb-benchmark.sh ipv4 diff --git a/binding/erlang/include/ip2region.hrl b/binding/erlang/include/ip2region.hrl index 1cf474f..b08a7f4 100644 --- a/binding/erlang/include/ip2region.hrl +++ b/binding/erlang/include/ip2region.hrl @@ -9,15 +9,6 @@ -define(XDB_VECTOR_INDEX_SIZE, 8). -define(XDB_VECTOR_INDEX_COUNT, (16#10000)). %% 256*256 -%% xdb header field offsets (bytes) --define(XDB_HEADER_VERSION_OFFSET, 0). --define(XDB_HEADER_INDEX_POLICY_OFFSET, 2). --define(XDB_HEADER_CREATED_AT_OFFSET, 4). --define(XDB_HEADER_START_INDEX_PTR_OFFSET, 8). --define(XDB_HEADER_END_INDEX_PTR_OFFSET, 12). --define(XDB_HEADER_IP_VERSION_OFFSET, 16). --define(XDB_HEADER_RUNTIME_PTR_BYTES_OFFSET, 18). - %% IP versions as stored in xdb header -define(IP_VERSION_4, 4). -define(IP_VERSION_6, 6). @@ -34,9 +25,18 @@ -define(IP2REGION_CACHE_V4, ets_ip2region_cache_v4). -define(IP2REGION_CACHE_V6, ets_ip2region_cache_v6). -%% Per-version pool names. ?IP2REGION_POOL is kept as the legacy v4 pool name. --define(IP2REGION_POOL, ip2region_pool). +%% 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() +}). + -endif. diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl index a111589..1ddd2cc 100644 --- a/binding/erlang/src/ip2region_sup.erl +++ b/binding/erlang/src/ip2region_sup.erl @@ -58,16 +58,9 @@ ensure_table(Name, Opts) -> pool_child_specs() -> {ok, DbConfig} = application:get_env(db), {ok, PoolArgsCfg} = application:get_env(poolargs), - Versions = [Version || {Version, _File} <- DbConfig], - UseLegacyName = (Versions == [ipv4]), - V4PoolName = case UseLegacyName of - true -> ?IP2REGION_POOL; - false -> ?IP2REGION_POOL_V4 - end, - ok = application:set_env(?APP_NAME, v4_pool_name, V4PoolName), lists:foldl( fun({ipv4, File}, Acc) -> - [make_pool_spec(V4PoolName, ipv4, File, PoolArgsCfg) | Acc]; + [make_pool_spec(?IP2REGION_POOL_V4, ipv4, File, PoolArgsCfg) | Acc]; ({ipv6, File}, Acc) -> [make_pool_spec(?IP2REGION_POOL_V6, ipv6, File, PoolArgsCfg) | Acc]; (_, Acc) -> diff --git a/binding/erlang/src/ip2region_worker.erl b/binding/erlang/src/ip2region_worker.erl index 216fd76..b0e3bf9 100644 --- a/binding/erlang/src/ip2region_worker.erl +++ b/binding/erlang/src/ip2region_worker.erl @@ -134,6 +134,7 @@ do_info(Info, State) -> error_logger:error_report(io:format("unknown info: ~p", [Info])), {noreply, State}. +-spec resolve_version(#xdb_header{}) -> {ok, ipv4 | ipv6} | {error, term()}. resolve_version(Header) -> case ip2region_xdb:header_version(Header) of 2 -> {ok, ipv4}; @@ -147,6 +148,10 @@ resolve_version(Header) -> end. load_vector_index(IoDevice, Version) -> + %% The vector index ETS table is global and shared by all pool workers. + %% Only the first worker (or the first worker after a restart) populates it. + %% Subsequent workers skip the file:read here; search_ip/2 uses file:pread/3, + %% so the current file pointer position does not affect correctness. Table = ip2region_xdb:vector_index_table(Version), case ets:info(Table, size) of undefined -> diff --git a/binding/erlang/src/ip2region_xdb.erl b/binding/erlang/src/ip2region_xdb.erl index eea6999..383f318 100644 --- a/binding/erlang/src/ip2region_xdb.erl +++ b/binding/erlang/src/ip2region_xdb.erl @@ -18,16 +18,6 @@ -include("ip2region.hrl"). --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() -}). - -spec parse_header(binary()) -> {ok, #xdb_header{}} | {error, invalid_header}. parse_header(Bin) when is_binary(Bin), byte_size(Bin) >= 20 -> < B. 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. diff --git a/binding/erlang/src/xdb.erl b/binding/erlang/src/xdb.erl index d160382..fa2ff83 100644 --- a/binding/erlang/src/xdb.erl +++ b/binding/erlang/src/xdb.erl @@ -17,7 +17,7 @@ search(Ip) -> case ip2region_util:ip_to_bytes(Ip) of {ok, ipv4, IpBin} -> - do_search(v4_pool(), ipv4, IpBin); + do_search(?IP2REGION_POOL_V4, ipv4, IpBin); {ok, ipv6, IpBin} -> do_search(?IP2REGION_POOL_V6, ipv6, IpBin); Ret -> @@ -31,16 +31,13 @@ do_search(PoolName, Version, IpBin) -> _ -> Worker = poolboy:checkout(PoolName, true, infinity), try - Region = ip2region_worker:search(Worker, IpBin), - ets:insert(CacheTable, {IpBin, Region}), - Region + 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. - -v4_pool() -> - case application:get_env(?APP_NAME, v4_pool_name) of - {ok, PoolName} -> PoolName; - undefined -> ?IP2REGION_POOL - end. diff --git a/binding/erlang/test/ip2region_sup_test.erl b/binding/erlang/test/ip2region_sup_test.erl index 3ed6162..57cc9c3 100644 --- a/binding/erlang/test/ip2region_sup_test.erl +++ b/binding/erlang/test/ip2region_sup_test.erl @@ -6,6 +6,10 @@ pools_started_test() -> application:stop(ip2region), application:unload(ip2region), ok = application:load(ip2region), + %% NOTE: rebar3 compiles tests under _build/test/lib/ip2region via a + %% symlink back to the source tree, so ?FILE resolves to the original + %% binding/erlang/test/... path. If rebar3 is configured to copy + %% instead of symlink, this path traversal must be adjusted. TestDir = filename:dirname(?FILE), ErlangDir = filename:dirname(TestDir), BindingDir = filename:dirname(ErlangDir), From 507b7bddc1c3de6a0fdd092f92b4e78b9e015c6c Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 04:54:08 +0900 Subject: [PATCH 13/23] =?UTF-8?q?fix(erlang):=20post-review=20cleanups=20?= =?UTF-8?q?=E2=80=94=20pread,=20self-contained=20tests,=20poolboy=20app=20?= =?UTF-8?q?dep,=20dead=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- binding/erlang/rebar.config | 6 ++-- binding/erlang/src/ip2region.app.src | 5 ++-- binding/erlang/src/ip2region_sup.erl | 4 +-- binding/erlang/src/ip2region_worker.erl | 12 ++++---- binding/erlang/src/xdb_benchmark.erl | 2 +- binding/erlang/test/ip2region_sup_test.erl | 6 ++-- binding/erlang/test/xdb_test.erl | 34 +++++++++++++++++----- 7 files changed, 45 insertions(+), 24 deletions(-) diff --git a/binding/erlang/rebar.config b/binding/erlang/rebar.config index 076f916..dd1affa 100644 --- a/binding/erlang/rebar.config +++ b/binding/erlang/rebar.config @@ -1,6 +1,6 @@ {erl_opts, [ - debug_info, - export_all, + debug_info, + export_all, nowarn_export_all ]}. @@ -18,7 +18,7 @@ {ex_doc, [ {extras, ["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}]}. diff --git a/binding/erlang/src/ip2region.app.src b/binding/erlang/src/ip2region.app.src index 8619174..9188b34 100644 --- a/binding/erlang/src/ip2region.app.src +++ b/binding/erlang/src/ip2region.app.src @@ -5,7 +5,8 @@ {mod, {ip2region_app, []}}, {applications, [kernel, - stdlib + stdlib, + poolboy ]}, {env,[ {poolargs, [ @@ -19,5 +20,5 @@ {modules, []}, {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"}]} ]}. diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl index 1ddd2cc..05ad9db 100644 --- a/binding/erlang/src/ip2region_sup.erl +++ b/binding/erlang/src/ip2region_sup.erl @@ -67,12 +67,12 @@ pool_child_specs() -> Acc end, [], DbConfig). -make_pool_spec(PoolName, Version, File, PoolArgsCfg) -> +make_pool_spec(PoolName, _Version, File, PoolArgsCfg) -> PoolArgs = [ {strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg ], - WorkerArgs = [{xdb_file, File}, {version, Version}], + WorkerArgs = [{xdb_file, File}], poolboy:child_spec(PoolName, PoolArgs, WorkerArgs). diff --git a/binding/erlang/src/ip2region_worker.erl b/binding/erlang/src/ip2region_worker.erl index b0e3bf9..695c1b4 100644 --- a/binding/erlang/src/ip2region_worker.erl +++ b/binding/erlang/src/ip2region_worker.erl @@ -124,7 +124,7 @@ do_call(stop, _From, State) -> do_call(Request, From, State) -> 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) -> error_logger:error_report(io_lib:format("unknown msg: ~p", [Msg])), @@ -154,14 +154,15 @@ load_vector_index(IoDevice, Version) -> %% so the current file pointer position does not affect correctness. Table = ip2region_xdb:vector_index_table(Version), case ets:info(Table, size) of + ?XDB_VECTOR_INDEX_COUNT -> + ok; undefined -> Opts = [named_table, set, public, {read_concurrency, true}, {keypos, 1}], ets:new(Table, Opts), load_vector_index_data(IoDevice, Table); - 0 -> - load_vector_index_data(IoDevice, Table); _ -> - ok + %% A previous worker may have crashed midway; reload to be safe. + load_vector_index_data(IoDevice, Table) end. load_vector_index_data(IoDevice, Table) -> @@ -215,8 +216,7 @@ ip_in_range(Ip, SIp, EIp, ipv6) -> end. read_file(IoDevice, Position, DataLength) -> - file:position(IoDevice, {bof, Position}), - file:read(IoDevice, DataLength). + file:pread(IoDevice, Position, DataLength). read_segment_index(IoDevice, SPtr, SegSize, SegmentTable) -> case ets:lookup(SegmentTable, SPtr) of diff --git a/binding/erlang/src/xdb_benchmark.erl b/binding/erlang/src/xdb_benchmark.erl index 0aa22db..36d94d8 100644 --- a/binding/erlang/src/xdb_benchmark.erl +++ b/binding/erlang/src/xdb_benchmark.erl @@ -14,7 +14,7 @@ main(DataFile) -> %% Keep benchmark output clean while still surfacing real errors. _ = logger:set_handler_config(default, level, error), _ = logger:set_primary_config(level, error), - application:ensure_started(ip2region), + {ok, _} = application:ensure_all_started(ip2region), show_hw_sw_info(), IpList = load_test_data(DataFile), run(IpList). diff --git a/binding/erlang/test/ip2region_sup_test.erl b/binding/erlang/test/ip2region_sup_test.erl index 57cc9c3..a1b73bd 100644 --- a/binding/erlang/test/ip2region_sup_test.erl +++ b/binding/erlang/test/ip2region_sup_test.erl @@ -3,8 +3,8 @@ -include("ip2region.hrl"). pools_started_test() -> - application:stop(ip2region), - application:unload(ip2region), + try application:stop(ip2region) catch _:_ -> ok end, + try application:unload(ip2region) catch _:_ -> ok end, ok = application:load(ip2region), %% NOTE: rebar3 compiles tests under _build/test/lib/ip2region via a %% symlink back to the source tree, so ?FILE resolves to the original @@ -19,6 +19,6 @@ pools_started_test() -> {ipv4, "ip2region.xdb"}, {ipv6, V6File} ]), - application:ensure_started(ip2region), + {ok, _} = application:ensure_all_started(ip2region), ?assert(is_pid(whereis(?IP2REGION_POOL_V4))), ?assert(is_pid(whereis(?IP2REGION_POOL_V6))). diff --git a/binding/erlang/test/xdb_test.erl b/binding/erlang/test/xdb_test.erl index aea2ab2..a725709 100644 --- a/binding/erlang/test/xdb_test.erl +++ b/binding/erlang/test/xdb_test.erl @@ -2,8 +2,10 @@ -include_lib("eunit/include/eunit.hrl"). +-define(IPV6_RESULT, "United States|Florida|Miami|Google LLC|US"). + search_test_() -> - application:ensure_started(ip2region), + {ok, _} = application:ensure_all_started(ip2region), A = "中国|广东省|广州市|中国电信|CN", Region0 = xdb:search("1.0.8.0"), Region1 = xdb:search(<<"1.0.8.0">>), @@ -17,17 +19,35 @@ search_test_() -> ]. ipv6_search_test_() -> - application:ensure_started(ip2region), + setup_ipv6(), [ - ?_assert(is_list(xdb:search("2001:4860:4860::8888"))), - ?_assert(is_list(xdb:search(<<"2001:4860:4860::8888">>))), - ?_assert(is_list(xdb:search({8193, 10304, 10304, 0, 0, 0, 0, 34952}))) + ?_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_() -> - application:ensure_started(ip2region), + {ok, _} = application:ensure_all_started(ip2region), [ ?_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})) - ]. \ No newline at end of file + ]. + +setup_ipv6() -> + try application:stop(ip2region) catch _:_ -> ok end, + try application:unload(ip2region) catch _:_ -> ok end, + ok = application:load(ip2region), + RepoRoot = repo_root(), + V6File = filename:join([RepoRoot, "data", "ip2region_v6.xdb"]), + ok = application:set_env(ip2region, db, [ + {ipv4, "ip2region.xdb"}, + {ipv6, V6File} + ]), + {ok, _} = application:ensure_all_started(ip2region). + +repo_root() -> + TestDir = filename:dirname(?FILE), + ErlangDir = filename:dirname(TestDir), + BindingDir = filename:dirname(ErlangDir), + filename:dirname(BindingDir). From 546ce589a927c378f3ff6fc807b1416765550f2f Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 04:58:45 +0900 Subject: [PATCH 14/23] docs(erlang): update README examples for IPv6 and current test output --- binding/erlang/README.md | 34 +++++++++------------- binding/erlang/README_zh.md | 56 +++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 48 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 29f363a..36a4199 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -55,7 +55,7 @@ Place the xdb file in the `priv` directory, then start the Erlang node: $ 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: ``` 1> xdb:search("1.0.8.0"). @@ -76,11 +76,11 @@ With dual-stack enabled, IPv6 addresses are supported in the same way: ``` 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">>)]). -... -3> io:format("~ts~n", [xdb:search({8193,10304,10304,0,0,0,0,34952})]). -... +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 @@ -95,22 +95,14 @@ With dual-stack enabled, IPv6 addresses are supported in the same way: * Start the ip2region Application -``` -...... - -application:ensure_started(ip2region), - -...... +```erlang +{ok, _} = application:ensure_all_started(ip2region). ``` * Call the `xdb:search/1` interface to query IP information -``` -...... - -ip2region:search("1.0.8.0"), - -...... +```erlang +xdb:search("1.0.8.0"). ``` ### Unit Test @@ -121,12 +113,12 @@ $ rebar3 eunit ===> Analyzing applications... ===> Compiling ip2region ===> Performing EUnit tests... -=INFO REPORT==== 17-Jan-2023::11:52:59.920155 === -XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb +=INFO REPORT==== 28-Jun-2026::04:53:28 === +XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb .... -Finished in 0.150 seconds -34 tests, 0 failures +Finished in 0.192 seconds +38 tests, 0 failures ``` ### Benchmark diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index d482169..c5b5b45 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -49,20 +49,30 @@ $ rebar3 compile ``` $ rebar3 shell ``` -在erlang shell中调用xdb:search/1接口查询Ip地址信息, 该接口支持以list格式字符串、binary格式字符串、tuple和整数表示的IP地址,如下: +在 Erlang shell 中调用 `xdb:search/1` 接口查询 IP 地址信息。该接口支持 list 字符串、binary 字符串、tuple 和整数表示的 IP 地址: ``` 1> xdb:search("1.0.8.0"). -[20013,22269,124,48,124,24191,19996,30465,124,24191,24030, - 24066,124,30005,20449] +[20013,22269,124,24191,19996,30465,124,24191,24030,24066, + 124,20013,22269,30005,20449,124,67,78] 2> 3> io:format("~ts~n", [xdb:search("1.0.8.0")]). -中国|0|广东省|广州市|电信 -io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]). -中国|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">>)]). +中国|广东省|广州市|中国电信|CN +5> io:format("~ts~n", [xdb:search({1,0,8,0})]). +中国|广东省|广州市|中国电信|CN 6> io:format("~ts~n", [xdb:search(16779264)]). -中国|0|广东省|广州市|电信 +中国|广东省|广州市|中国电信|CN +``` + +启用双栈后,IPv6 地址也按同样方式支持: +``` +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 ``` ### 使用方法 @@ -72,22 +82,14 @@ io:format("~ts~n", [xdb:search(<<"1.0.8.0">>)]). ip2region ]}. ``` -* 启动ip2region Application -``` -...... - -application:ensure_started(ip2region), - -...... +* 启动 ip2region Application +```erlang +{ok, _} = application:ensure_all_started(ip2region). ``` -* 调用xdb:search/1接口查询IP信息 -``` -...... - -ip2region:search("1.0.8.0"), - -...... +* 调用 `xdb:search/1` 接口查询 IP 信息 +```erlang +xdb:search("1.0.8.0"). ``` ### 单元测试 @@ -98,12 +100,12 @@ $ rebar3 eunit ===> Analyzing applications... ===> Compiling ip2region ===> Performing EUnit tests... -=INFO REPORT==== 17-Jan-2023::11:52:59.920155 === -XdbFile:/home/admin/erl-workspace/ip2region/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb +=INFO REPORT==== 28-Jun-2026::04:53:28 === +XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erlang/_build/test/lib/ip2region/priv/ip2region.xdb .... -Finished in 0.074 seconds -4 tests, 0 failures +Finished in 0.192 seconds +38 tests, 0 failures ``` ### 基准测试 From 44013251b6b1bb1adc11521c03609821d14b21eb Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:09:38 +0900 Subject: [PATCH 15/23] test(erlang): fix integer ip_version assertion and add invalid format cases --- binding/erlang/test/ip2region_util_test.erl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/binding/erlang/test/ip2region_util_test.erl b/binding/erlang/test/ip2region_util_test.erl index ff8d0f2..b7df998 100644 --- a/binding/erlang/test/ip2region_util_test.erl +++ b/binding/erlang/test/ip2region_util_test.erl @@ -30,6 +30,7 @@ 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)), @@ -37,6 +38,16 @@ invalid_inputs_test_() -> ?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n(16#100000000)) ]. +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(16#100000000)) + ]. + legacy_ipv4_to_n_test_() -> [ ?_assertEqual(16779264, ip2region_util:ipv4_to_n("1.0.8.0")), From d1dda93abd36b5e6fefaf74647c79f7ca239cb9e Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:10:04 +0900 Subject: [PATCH 16/23] fix(erlang): guard xdb:search against unconfigured pool --- binding/erlang/src/xdb.erl | 25 +++++++++++++++---------- binding/erlang/test/xdb_test.erl | 8 ++++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/binding/erlang/src/xdb.erl b/binding/erlang/src/xdb.erl index fa2ff83..66b1c0d 100644 --- a/binding/erlang/src/xdb.erl +++ b/binding/erlang/src/xdb.erl @@ -29,15 +29,20 @@ do_search(PoolName, Version, IpBin) -> case ets:lookup(CacheTable, IpBin) of [{_, Region}] -> Region; _ -> - 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) + 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. diff --git a/binding/erlang/test/xdb_test.erl b/binding/erlang/test/xdb_test.erl index a725709..aea2439 100644 --- a/binding/erlang/test/xdb_test.erl +++ b/binding/erlang/test/xdb_test.erl @@ -34,6 +34,14 @@ invalid_search_test_() -> ?_assertEqual({error, bad_ip_format}, xdb:search({1,2,3})) ]. +ipv6_pool_not_configured_test() -> + try application:stop(ip2region) catch _:_ -> ok end, + try application:unload(ip2region) catch _:_ -> ok end, + ok = application:load(ip2region), + ok = application:set_env(ip2region, db, [{ipv4, "ip2region.xdb"}]), + {ok, _} = application:ensure_all_started(ip2region), + ?assertEqual({error, pool_not_configured}, xdb:search("2001:4860:4860::8888")). + setup_ipv6() -> try application:stop(ip2region) catch _:_ -> ok end, try application:unload(ip2region) catch _:_ -> ok end, From fac3f73193663fc64fef53a3cb3d277a0a04ba9a Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:10:07 +0900 Subject: [PATCH 17/23] test(erlang): expand xdb header and table helper coverage --- binding/erlang/test/ip2region_xdb_test.erl | 49 +++++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/binding/erlang/test/ip2region_xdb_test.erl b/binding/erlang/test/ip2region_xdb_test.erl index 1ce5a70..cf3084c 100644 --- a/binding/erlang/test/ip2region_xdb_test.erl +++ b/binding/erlang/test/ip2region_xdb_test.erl @@ -1,16 +1,43 @@ -module(ip2region_xdb_test). --include_lib("eunit/include/eunit.hrl"). -header_parse_test() -> - HeaderBin = <<3:16/little, 1:16/little, 123456:32/little, - 1000:32/little, 2000:32/little, 6:16/little, 4:16/little, - 0:((256-20)*8)>>, +-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(6, ip2region_xdb:header_ip_version(Header)), - ?assertEqual(1000, ip2region_xdb:header_start_index_ptr(Header)), - ?assertEqual(2000, ip2region_xdb:header_end_index_ptr(Header)). + ?assertEqual(?IP_VERSION_6, ip2region_xdb:header_ip_version(Header)). -version_constants_test() -> - ?assertEqual(14, ip2region_xdb:segment_index_size(ipv4)), - ?assertEqual(38, ip2region_xdb:segment_index_size(ipv6)). +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)) + ]. From b1eb00e25e9356bd634915d3c69c100a87263a14 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:10:10 +0900 Subject: [PATCH 18/23] docs(erlang): update READMEs with IPv6 examples and benchmark output --- binding/erlang/README.md | 32 +++++++++++----------- binding/erlang/README_zh.md | 53 +++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 36a4199..3280193 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -1,10 +1,10 @@ :globe_with_meridians: [中文简体](README_zh.md) | [English](README.md) -# ip2region erlang query client +# ip2region Erlang query client ### 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 @@ -43,7 +43,7 @@ The `xdb:search/1` interface automatically detects IPv4 and IPv6 inputs and rout ### Compile -``` +```bash $ rebar3 compile ``` @@ -51,13 +51,13 @@ $ rebar3 compile Place the xdb file in the `priv` directory, then start the Erlang node: -``` +```bash $ 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: -``` +```erlang 1> xdb:search("1.0.8.0"). [20013,22269,124,24191,19996,30465,124,24191,24030,24066, 124,20013,22269,30005,20449,124,67,78] @@ -74,7 +74,7 @@ Call the `xdb:search/1` interface in the Erlang shell to query IP address inform 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">>)]). @@ -85,21 +85,21 @@ United States|Florida|Miami|Google LLC|US ### Usage -* Add the dependency in `rebar.config` +- Add the dependency in `rebar.config` -``` +```erlang {deps, [ ip2region ]}. ``` -* Start the ip2region Application +- Start the ip2region Application ```erlang {ok, _} = application:ensure_all_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"). @@ -107,7 +107,7 @@ xdb:search("1.0.8.0"). ### Unit Test -``` +```bash $ rebar3 eunit ===> Verifying dependencies... ===> Analyzing applications... @@ -129,27 +129,27 @@ Both IPv4 and IPv6 benchmarks share the same script. Run it with the desired IP > 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/ $ sh xdb-benchmark.sh ipv4 ``` For IPv6: -``` +```bash $ sh xdb-benchmark.sh ipv6 ``` Or use the Makefile targets from the `binding/erlang` directory: -``` +```bash $ make bench-v4 $ make bench-v6 ``` #### IPv4 benchmark example -``` +```bash System: CPU : Apple M4 Cores : 10 cores / 10 threads @@ -165,7 +165,7 @@ Done. #### IPv6 benchmark example -``` +```bash System: CPU : Apple M4 Cores : 10 cores / 10 threads diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index c5b5b45..faba2db 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -1,13 +1,16 @@ :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,[ {poolargs, [ {size, 1}, %% 工作进程默认数量 @@ -23,7 +26,7 @@ 如需启用 IPv6 查询,在 `db` 列表中加入 `ipv6` 项,并将两个 xdb 文件放到 `priv/` 目录下: -``` erlang +```erlang {env,[ {poolargs, [ {size, 1}, @@ -40,17 +43,21 @@ ### 编译 -``` +```bash $ rebar3 compile ``` ### 运行 -将xdb文件放到priv目录下,然后启动erlang节点: -``` + +将 xdb 文件放到 `priv` 目录下,然后启动 Erlang 节点: + +```bash $ rebar3 shell ``` + 在 Erlang shell 中调用 `xdb:search/1` 接口查询 IP 地址信息。该接口支持 list 字符串、binary 字符串、tuple 和整数表示的 IP 地址: -``` + +```erlang 1> xdb:search("1.0.8.0"). [20013,22269,124,24191,19996,30465,124,24191,24030,24066, 124,20013,22269,30005,20449,124,67,78] @@ -66,7 +73,8 @@ $ rebar3 shell ``` 启用双栈后,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">>)]). @@ -76,25 +84,30 @@ United States|Florida|Miami|Google LLC|US ``` ### 使用方法 -* 在rebar.config中引入依赖 -``` + +- 在 `rebar.config` 中引入依赖 + +```erlang {deps, [ ip2region ]}. ``` -* 启动 ip2region Application + +- 启动 ip2region Application + ```erlang {ok, _} = application:ensure_all_started(ip2region). ``` -* 调用 `xdb:search/1` 接口查询 IP 信息 +- 调用 `xdb:search/1` 接口查询 IP 信息 + ```erlang xdb:search("1.0.8.0"). ``` ### 单元测试 -``` +```bash $ rebar3 eunit ===> Verifying dependencies... ===> Analyzing applications... @@ -115,27 +128,27 @@ IPv4 与 IPv6 共用一个脚本,通过参数指定版本: > `cold` = 第一次遍历源文件:每个 IP 都会触发真实查询,并把结果写入 ETS 缓存。 > `warm` = 第二次遍历同一列表,所有查询都直接命中 ETS 缓存。 -``` +```bash $ cd benchmarks/ $ sh xdb-benchmark.sh ipv4 ``` IPv6: -``` +```bash $ sh xdb-benchmark.sh ipv6 ``` 也可以在 `binding/erlang` 目录直接用 Makefile: -``` +```bash $ make bench-v4 $ make bench-v6 ``` #### IPv4 基准测试示例 -``` +```bash System: CPU : Apple M4 Cores : 10 cores / 10 threads @@ -151,7 +164,7 @@ Done. #### IPv6 基准测试示例 -``` +```bash System: CPU : Apple M4 Cores : 10 cores / 10 threads From e50ead2cbaf08381915d77fa652b8254993d7700 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:17:55 +0900 Subject: [PATCH 19/23] fix(erlang): validate tuple segment ranges in ip2region_util --- binding/erlang/src/ip2region_util.erl | 15 ++++++++++----- binding/erlang/test/ip2region_util_test.erl | 11 ++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/binding/erlang/src/ip2region_util.erl b/binding/erlang/src/ip2region_util.erl index db5ec46..d84dd56 100644 --- a/binding/erlang/src/ip2region_util.erl +++ b/binding/erlang/src/ip2region_util.erl @@ -11,11 +11,15 @@ -export([ipv4_to_n/1, ip_version/1, ip_to_bytes/1]). +-define(IS_UINT8(V), is_integer(V), V >= 0, V =< 255). +-define(IS_UINT16(V), is_integer(V), V >= 0, V =< 65535). + -spec ip_version(Ip :: tuple() | list() | binary() | integer()) -> ipv4 | ipv6 | {error, atom()}. ip_version(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF -> ipv4; -ip_version({_, _, _, _}) -> ipv4; -ip_version({_, _, _, _, _, _, _, _}) -> ipv6; +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) -> @@ -31,9 +35,10 @@ ip_version(_) -> {ok, ipv4 | ipv6, binary()} | {error, atom()}. ip_to_bytes(Ip) when is_integer(Ip), Ip >= 0, Ip =< 16#FFFFFFFF -> {ok, ipv4, <>}; -ip_to_bytes({A, B, C, D}) -> +ip_to_bytes({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) -> {ok, ipv4, <>}; -ip_to_bytes({A, B, C, D, E, F, G, H}) -> +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, <>}; ip_to_bytes(Ip) when is_binary(Ip) -> ip_to_bytes(binary_to_list(Ip)); @@ -52,7 +57,7 @@ ip_to_bytes(_) -> -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}) -> +ipv4_to_n({A, B, C, D}) when ?IS_UINT8(A), ?IS_UINT8(B), ?IS_UINT8(C), ?IS_UINT8(D) -> <> = <>, N; ipv4_to_n(Ip) when is_binary(Ip) -> diff --git a/binding/erlang/test/ip2region_util_test.erl b/binding/erlang/test/ip2region_util_test.erl index b7df998..85880de 100644 --- a/binding/erlang/test/ip2region_util_test.erl +++ b/binding/erlang/test/ip2region_util_test.erl @@ -34,8 +34,15 @@ invalid_inputs_test_() -> ?_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(16#100000000)), + ?_assertEqual({error, bad_ip_format}, ip2region_util:ipv4_to_n({256, 0, 0, 1})) ]. ip_version_test_() -> @@ -45,6 +52,8 @@ ip_version_test_() -> ?_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)) ]. From daf6749c34f54d18296605edbe30ccd9dfa0fe0d Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:17:59 +0900 Subject: [PATCH 20/23] fix(erlang): pass expected version to worker and fail fast on xdb mismatch --- binding/erlang/src/ip2region_sup.erl | 4 +-- binding/erlang/src/ip2region_worker.erl | 6 +++- binding/erlang/test/xdb_test.erl | 41 ++++++++++--------------- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/binding/erlang/src/ip2region_sup.erl b/binding/erlang/src/ip2region_sup.erl index 05ad9db..7b07742 100644 --- a/binding/erlang/src/ip2region_sup.erl +++ b/binding/erlang/src/ip2region_sup.erl @@ -67,12 +67,12 @@ pool_child_specs() -> Acc end, [], DbConfig). -make_pool_spec(PoolName, _Version, File, PoolArgsCfg) -> +make_pool_spec(PoolName, Version, File, PoolArgsCfg) -> PoolArgs = [ {strategy, fifo}, {name, {local, PoolName}}, {worker_module, ip2region_worker} | PoolArgsCfg ], - WorkerArgs = [{xdb_file, File}], + WorkerArgs = [{xdb_file, File}, {expected_version, Version}], poolboy:child_spec(PoolName, PoolArgs, WorkerArgs). diff --git a/binding/erlang/src/ip2region_worker.erl b/binding/erlang/src/ip2region_worker.erl index 695c1b4..90d9152 100644 --- a/binding/erlang/src/ip2region_worker.erl +++ b/binding/erlang/src/ip2region_worker.erl @@ -62,10 +62,14 @@ init(Args) -> {ok, IoDevice} = file:open(XdbFileName, [read, binary]), {ok, HeaderBin} = file:read(IoDevice, ?XDB_HEADER_SIZE), {ok, Header} = ip2region_xdb:parse_header(HeaderBin), + ExpectedVersion = proplists:get_value(expected_version, Args), case resolve_version(Header) of - {ok, Version} -> + {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} diff --git a/binding/erlang/test/xdb_test.erl b/binding/erlang/test/xdb_test.erl index aea2439..5388f6e 100644 --- a/binding/erlang/test/xdb_test.erl +++ b/binding/erlang/test/xdb_test.erl @@ -5,7 +5,7 @@ -define(IPV6_RESULT, "United States|Florida|Miami|Google LLC|US"). search_test_() -> - {ok, _} = application:ensure_all_started(ip2region), + {ok, _} = ip2region_test_util:start_app(), A = "中国|广东省|广州市|中国电信|CN", Region0 = xdb:search("1.0.8.0"), Region1 = xdb:search(<<"1.0.8.0">>), @@ -19,7 +19,7 @@ search_test_() -> ]. ipv6_search_test_() -> - setup_ipv6(), + {ok, _} = ip2region_test_util:start_app(), [ ?_assert(?IPV6_RESULT =:= xdb:search("2001:4860:4860::8888")), ?_assert(?IPV6_RESULT =:= xdb:search(<<"2001:4860:4860::8888">>)), @@ -27,7 +27,7 @@ ipv6_search_test_() -> ]. invalid_search_test_() -> - {ok, _} = application:ensure_all_started(ip2region), + {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")), @@ -35,27 +35,20 @@ invalid_search_test_() -> ]. ipv6_pool_not_configured_test() -> - try application:stop(ip2region) catch _:_ -> ok end, - try application:unload(ip2region) catch _:_ -> ok end, - ok = application:load(ip2region), - ok = application:set_env(ip2region, db, [{ipv4, "ip2region.xdb"}]), - {ok, _} = application:ensure_all_started(ip2region), + {ok, _} = ip2region_test_util:start_app([{ipv4, "ip2region.xdb"}]), ?assertEqual({error, pool_not_configured}, xdb:search("2001:4860:4860::8888")). -setup_ipv6() -> - try application:stop(ip2region) catch _:_ -> ok end, - try application:unload(ip2region) catch _:_ -> ok end, - ok = application:load(ip2region), - RepoRoot = repo_root(), - V6File = filename:join([RepoRoot, "data", "ip2region_v6.xdb"]), - ok = application:set_env(ip2region, db, [ - {ipv4, "ip2region.xdb"}, - {ipv6, V6File} +xdb_version_mismatch_test() -> + {ok, _} = ip2region_test_util:start_app(), + V4File = filename:join([ + ip2region_test_util:repo_root(), "binding", "erlang", "priv", "ip2region.xdb" ]), - {ok, _} = application:ensure_all_started(ip2region). - -repo_root() -> - TestDir = filename:dirname(?FILE), - ErlangDir = filename:dirname(TestDir), - BindingDir = filename:dirname(ErlangDir), - filename:dirname(BindingDir). + 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. From d06690f6518350d13cf9531581e6cd5b96352a88 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:18:03 +0900 Subject: [PATCH 21/23] test(erlang): add shared test helper and DRY app setup --- binding/erlang/test/ip2region_sup_test.erl | 18 +------- binding/erlang/test/ip2region_test_util.erl | 51 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 binding/erlang/test/ip2region_test_util.erl diff --git a/binding/erlang/test/ip2region_sup_test.erl b/binding/erlang/test/ip2region_sup_test.erl index a1b73bd..9c86e12 100644 --- a/binding/erlang/test/ip2region_sup_test.erl +++ b/binding/erlang/test/ip2region_sup_test.erl @@ -3,22 +3,6 @@ -include("ip2region.hrl"). pools_started_test() -> - try application:stop(ip2region) catch _:_ -> ok end, - try application:unload(ip2region) catch _:_ -> ok end, - ok = application:load(ip2region), - %% NOTE: rebar3 compiles tests under _build/test/lib/ip2region via a - %% symlink back to the source tree, so ?FILE resolves to the original - %% binding/erlang/test/... path. If rebar3 is configured to copy - %% instead of symlink, this path traversal must be adjusted. - TestDir = filename:dirname(?FILE), - ErlangDir = filename:dirname(TestDir), - BindingDir = filename:dirname(ErlangDir), - RepoRoot = filename:dirname(BindingDir), - V6File = filename:join([RepoRoot, "data", "ip2region_v6.xdb"]), - ok = application:set_env(ip2region, db, [ - {ipv4, "ip2region.xdb"}, - {ipv6, V6File} - ]), - {ok, _} = application:ensure_all_started(ip2region), + {ok, _} = ip2region_test_util:start_app(), ?assert(is_pid(whereis(?IP2REGION_POOL_V4))), ?assert(is_pid(whereis(?IP2REGION_POOL_V6))). diff --git a/binding/erlang/test/ip2region_test_util.erl b/binding/erlang/test/ip2region_test_util.erl new file mode 100644 index 0000000..4e7417b --- /dev/null +++ b/binding/erlang/test/ip2region_test_util.erl @@ -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). From 30ed0cf028bf5ee5c367535fd6b3fa0793192f29 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:18:08 +0900 Subject: [PATCH 22/23] docs(erlang): update README test counts to 63 --- binding/erlang/README.md | 2 +- binding/erlang/README_zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/binding/erlang/README.md b/binding/erlang/README.md index 3280193..15cbe74 100644 --- a/binding/erlang/README.md +++ b/binding/erlang/README.md @@ -118,7 +118,7 @@ XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erla .... Finished in 0.192 seconds -38 tests, 0 failures +63 tests, 0 failures ``` ### Benchmark diff --git a/binding/erlang/README_zh.md b/binding/erlang/README_zh.md index faba2db..bfea45b 100644 --- a/binding/erlang/README_zh.md +++ b/binding/erlang/README_zh.md @@ -118,7 +118,7 @@ XdbFile:/Users/nana/Documents/code/ip2region/.worktrees/erlang-ipv6/binding/erla .... Finished in 0.192 seconds -38 tests, 0 failures +63 tests, 0 failures ``` ### 基准测试 From dbef88e337050ae5e6be53286e553d3b098d92f8 Mon Sep 17 00:00:00 2001 From: Alice39s Date: Sun, 28 Jun 2026 05:18:11 +0900 Subject: [PATCH 23/23] chore: fix trailing whitespace on worktree entries in .gitignore --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index ab5f10d..37ea030 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,6 @@ target #vscode .vscode build - -# git worktrees -.worktrees/ + +# git worktrees +.worktrees/