11defmodule Geo.Geography.Country.Cache.Server do
22 @ moduledoc """
33 GenServer that caches country data in memory for fast lookup and search operations.
4- Loads all countries once at startup and provides efficient search functions .
4+ Uses lazy loading - countries are loaded on first access rather than at startup .
55 Designed to work as a pooled worker with Poolboy - no longer uses named registration.
6+
7+ The server will automatically stop after @stop_interval once countries are loaded.
68 """
79
810 use GenServer
911 require Logger
1012
11- @ refresh_interval :timer . minutes ( 30 )
13+ @ stop_interval :timer . minutes ( 1 )
1214
1315 defmodule State do
1416 @ moduledoc """
1517 State structure for the Country Cache Server.
18+ All country-related fields are nil until first access (lazy loading).
19+ The timer_ref is nil until countries are loaded, then starts the stop timer.
1620 """
1721 defstruct [
1822 :countries_list_by_iso_code ,
@@ -24,11 +28,11 @@ defmodule Geo.Geography.Country.Cache.Server do
2428 ]
2529
2630 @ type t :: % __MODULE__ {
27- countries_list_by_iso_code: [ Geo.Geography.Country . t ( ) ] ,
28- countries_list_by_name: [ Geo.Geography.Country . t ( ) ] ,
29- countries_map_by_iso_code: % { String . t ( ) => Geo.Geography.Country . t ( ) } ,
30- countries_map_by_name: % { String . t ( ) => Geo.Geography.Country . t ( ) } ,
31- last_refresh: DateTime . t ( ) ,
31+ countries_list_by_iso_code: [ Geo.Geography.Country . t ( ) ] | nil ,
32+ countries_list_by_name: [ Geo.Geography.Country . t ( ) ] | nil ,
33+ countries_map_by_iso_code: % { String . t ( ) => Geo.Geography.Country . t ( ) } | nil ,
34+ countries_map_by_name: % { String . t ( ) => Geo.Geography.Country . t ( ) } | nil ,
35+ last_refresh: DateTime . t ( ) | nil ,
3236 timer_ref: reference ( ) | nil
3337 }
3438 end
@@ -39,104 +43,80 @@ defmodule Geo.Geography.Country.Cache.Server do
3943
4044 @ impl true
4145 def init ( _opts ) do
42- try do
43- state = load_countries! ( )
44-
45- # Schedule periodic refresh
46- timer_ref = Process . send_after ( self ( ) , :refresh , @ refresh_interval )
47- state = % { state | timer_ref: timer_ref }
48-
49- Logger . info ( "Cache worker started successfully" )
50- { :ok , state }
51- rescue
52- error ->
53- Logger . error ( "Failed to initialize cache worker: #{ inspect ( error ) } " )
54- { :stop , error }
55- end
46+ # Start with empty state - countries will be loaded on first access
47+ # Timer will only start when countries are actually loaded
48+ state = % State {
49+ countries_list_by_iso_code: nil ,
50+ countries_list_by_name: nil ,
51+ countries_map_by_iso_code: nil ,
52+ countries_map_by_name: nil ,
53+ last_refresh: nil ,
54+ timer_ref: nil
55+ }
56+
57+ Logger . info ( "Cache worker started successfully: #{ inspect ( self ( ) ) } " )
58+ { :ok , state }
5659 end
5760
5861 @ impl true
5962 def handle_call ( :search_all , _from , state ) do
60- { :reply , do_search_all ( state ) , state }
63+ state = ensure_countries_loaded ( state )
64+ result = do_search_all ( state )
65+ { :reply , result , state }
6166 end
6267
6368 @ impl true
6469 def handle_call ( { :search , query } , _from , state ) do
70+ state = ensure_countries_loaded ( state )
6571 { :reply , do_search ( query , state ) , state }
6672 end
6773
6874 @ impl true
6975 def handle_call ( { :get_by_iso_code , iso_code } , _from , state ) do
70- country = Map . get ( state . countries_map_by_iso_code , String . downcase ( iso_code ) )
71- { :reply , country , state }
72- end
76+ state = ensure_countries_loaded ( state )
7377
74- @ impl true
75- def handle_call ( :refresh , _from , state ) do
76- try do
77- # Cancel existing timer if it exists
78- if state . timer_ref do
79- Process . cancel_timer ( state . timer_ref )
78+ country =
79+ if state . countries_map_by_iso_code do
80+ Map . get ( state . countries_map_by_iso_code , String . downcase ( iso_code ) )
81+ else
82+ nil
8083 end
8184
82- new_state = load_countries! ( )
83-
84- # Schedule next refresh
85- timer_ref = Process . send_after ( self ( ) , :refresh , @ refresh_interval )
86- new_state = % { new_state | timer_ref: timer_ref }
87-
88- Logger . info ( "Cache worker refreshed successfully" )
89- { :reply , :ok , new_state }
90- rescue
91- error ->
92- Logger . error ( "Failed to refresh cache worker: #{ inspect ( error ) } " )
93- { :reply , { :error , error } , state }
94- end
85+ { :reply , country , state }
9586 end
9687
9788 @ impl true
9889 def handle_call ( :status , _from , state ) do
99- status = % {
100- countries_count: map_size ( state . countries_map_by_iso_code ) ,
101- last_refresh: state . last_refresh ,
102- worker_pid: self ( )
103- }
90+ status =
91+ if state . last_refresh do
92+ % {
93+ countries_count: map_size ( state . countries_map_by_iso_code ) ,
94+ last_refresh: state . last_refresh ,
95+ worker_pid: self ( ) ,
96+ loaded: true
97+ }
98+ else
99+ % {
100+ countries_count: 0 ,
101+ last_refresh: nil ,
102+ worker_pid: self ( ) ,
103+ loaded: false
104+ }
105+ end
104106
105107 { :reply , status , state }
106108 end
107109
108110 @ impl true
109- def handle_info ( :refresh , state ) do
110- # Periodic refresh
111- try do
112- # Cancel existing timer if it exists
113- if state . timer_ref do
114- Process . cancel_timer ( state . timer_ref )
115- end
116-
117- new_state = load_countries! ( )
118-
119- Logger . debug ( "Cache worker auto-refreshed successfully" )
120-
121- # Schedule next refresh
122- timer_ref = Process . send_after ( self ( ) , :refresh , @ refresh_interval )
123- new_state = % { new_state | timer_ref: timer_ref }
124-
125- { :noreply , new_state }
126- rescue
127- error ->
128- Logger . warning ( "Failed to auto-refresh cache worker: #{ inspect ( error ) } " )
129-
130- # Still schedule next refresh attempt
131- timer_ref = Process . send_after ( self ( ) , :refresh , @ refresh_interval )
132- new_state = % { state | timer_ref: timer_ref }
133- { :noreply , new_state }
134- end
111+ def handle_info ( :stop , state ) do
112+ Logger . info ( "Cache worker stopping after #{ @ stop_interval } ms as scheduled" )
113+ { :stop , :normal , state }
135114 end
136115
137116 @ impl true
138- def terminate ( _reason , state ) do
139- # Cancel timer when GenServer is stopping
117+ def terminate ( reason , state ) do
118+ Logger . info ( "Cache worker #{ inspect ( self ( ) ) } terminating with reason: #{ inspect ( reason ) } " )
119+ # Cancel stop timer when GenServer is stopping
140120 if state . timer_ref do
141121 Process . cancel_timer ( state . timer_ref )
142122 end
@@ -145,8 +125,24 @@ defmodule Geo.Geography.Country.Cache.Server do
145125
146126 # Private functions
147127
148- # Returns a State struct with loaded countries data
149- defp load_countries! do
128+ # Ensures countries are loaded in the state, loading them if last_refresh is nil
129+ defp ensure_countries_loaded ( state ) do
130+ if state . last_refresh do
131+ state
132+ else
133+ try do
134+ load_countries! ( state )
135+ rescue
136+ error ->
137+ Logger . error ( "Failed to load countries: #{ inspect ( error ) } " )
138+ # Return state unchanged if loading fails
139+ state
140+ end
141+ end
142+ end
143+
144+ # Returns a State struct with loaded countries data, preserving existing state
145+ defp load_countries! ( existing_state ) do
150146 # Get countries sorted by iso_code (default sort from the resource)
151147 countries = Geo.Geography . list_countries! ( authorize?: false )
152148
@@ -167,17 +163,39 @@ defmodule Geo.Geography.Country.Cache.Server do
167163 { Ash.CiString . to_comparable_string ( country . name ) , country }
168164 end )
169165
166+ # Cancel existing timer if there is one
167+ if existing_state . timer_ref do
168+ Process . cancel_timer ( existing_state . timer_ref )
169+ end
170+
171+ # Start stop timer now that countries are loaded
172+ timer_ref = Process . send_after ( self ( ) , :stop , @ stop_interval )
173+ Logger . info ( "Countries loaded successfully, worker will stop in #{ @ stop_interval } ms" )
174+
170175 % State {
171176 countries_list_by_iso_code: countries_list_by_iso_code ,
172177 countries_list_by_name: countries_list_by_name ,
173178 countries_map_by_iso_code: countries_map_by_iso_code ,
174179 countries_map_by_name: countries_map_by_name ,
175180 last_refresh: DateTime . utc_now ( ) ,
176- timer_ref: nil
181+ timer_ref: timer_ref
177182 }
178183 end
179184
180185 defp do_search ( query , state ) do
186+ # If countries not loaded, return empty results
187+ if is_nil ( state . countries_map_by_name ) do
188+ % Geo.Geography.Country.Cache.SearchResult {
189+ by_iso_code: [ ] ,
190+ by_name: [ ]
191+ }
192+ else
193+ do_search_with_data ( query , state )
194+ end
195+ end
196+
197+ defp do_search_with_data ( query , state ) do
198+
181199 query_down = String . downcase ( query )
182200
183201 # Use exact match from countries_map_by_name for efficiency
@@ -271,10 +289,10 @@ defmodule Geo.Geography.Country.Cache.Server do
271289 end
272290
273291 defp do_search_all ( state ) do
274- # Return SearchResults struct with all countries
292+ # Return SearchResults struct with all countries, or empty if not loaded
275293 % Geo.Geography.Country.Cache.SearchResult {
276- by_iso_code: state . countries_list_by_iso_code ,
277- by_name: state . countries_list_by_name
294+ by_iso_code: state . countries_list_by_iso_code || [ ] ,
295+ by_name: state . countries_list_by_name || [ ]
278296 }
279297 end
280298end
0 commit comments