Skip to content

Commit dd29670

Browse files
committed
Misc
1 parent 1a1afce commit dd29670

19 files changed

Lines changed: 298 additions & 569 deletions

.cursor/rules/usage-rules.mdc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
---
2-
description: All rules from `mix usage_rules.sync`
3-
globs:
4-
alwaysApply: true
2+
alwaysApply: false
53
---
64
<-- usage-rules-start -->
75
<-- igniter-start -->

lib/geo/application.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,12 @@ defmodule Geo.Application do
5959
GeoWeb.Endpoint.config_change(changed, removed)
6060
:ok
6161
end
62+
63+
# Add shutdown logging
64+
@impl true
65+
def stop(reason) do
66+
require Logger
67+
Logger.info("Geo.Application terminating with reason: #{inspect(reason)}")
68+
:ok
69+
end
6270
end

lib/geo/geography/country/cache/server.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,11 @@ defmodule Geo.Geography.Country.Cache.Server do
115115

116116
@impl true
117117
def terminate(reason, state) do
118+
Logger.info("Cache worker terminate/2 called with reason: #{inspect(reason)} and pid: #{inspect(self())}")
118119
# Cancel stop timer when GenServer is stopping
119120
if state.timer_ref do
120121
Process.cancel_timer(state.timer_ref)
121122
end
122-
123-
Logger.info("Cache worker exiting with reason: #{inspect(reason)}")
124123
:ok
125124
end
126125

lib/geo/log_trimmer.ex

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
defmodule Geo.LogTrimmer do
2+
@moduledoc """
3+
A GenServer that periodically trims log files to prevent them from growing too large.
4+
5+
This process runs in the background and periodically checks the specified log file.
6+
If the file exceeds the maximum number of lines, it trims it to keep only the most recent lines.
7+
"""
8+
9+
use GenServer
10+
require Logger
11+
12+
@trim_interval :timer.minutes(5)
13+
@max_log_lines 10_000
14+
15+
# Client API
16+
17+
@doc """
18+
Starts the log trimmer for the specified log file.
19+
"""
20+
def start_link(log_file) when is_binary(log_file) do
21+
GenServer.start_link(__MODULE__, log_file, name: __MODULE__)
22+
end
23+
24+
@doc """
25+
Stops the log trimmer.
26+
"""
27+
def stop do
28+
if Process.whereis(__MODULE__) do
29+
GenServer.stop(__MODULE__)
30+
end
31+
end
32+
33+
@doc """
34+
Manually triggers a log trim operation.
35+
"""
36+
def trim_now do
37+
if Process.whereis(__MODULE__) do
38+
GenServer.cast(__MODULE__, :trim_now)
39+
end
40+
end
41+
42+
@doc """
43+
Gets the current status of the log trimmer.
44+
"""
45+
def status do
46+
if Process.whereis(__MODULE__) do
47+
GenServer.call(__MODULE__, :status)
48+
else
49+
{:error, :not_running}
50+
end
51+
end
52+
53+
# Server Callbacks
54+
55+
@impl GenServer
56+
def init(log_file) do
57+
# Schedule the first trim
58+
schedule_trim()
59+
60+
state = %{
61+
log_file: log_file,
62+
last_trim: DateTime.utc_now(),
63+
trim_count: 0
64+
}
65+
66+
Logger.info("LogTrimmer started for #{log_file}, trimming every #{div(@trim_interval, 60_000)} minutes")
67+
68+
{:ok, state}
69+
end
70+
71+
@impl GenServer
72+
def handle_info(:trim, state) do
73+
new_state = perform_trim(state)
74+
schedule_trim()
75+
{:noreply, new_state}
76+
end
77+
78+
@impl GenServer
79+
def handle_cast(:trim_now, state) do
80+
new_state = perform_trim(state)
81+
{:noreply, new_state}
82+
end
83+
84+
@impl GenServer
85+
def handle_call(:status, _from, state) do
86+
status = %{
87+
log_file: state.log_file,
88+
last_trim: state.last_trim,
89+
trim_count: state.trim_count,
90+
max_lines: @max_log_lines,
91+
trim_interval_minutes: div(@trim_interval, 60_000)
92+
}
93+
{:reply, {:ok, status}, state}
94+
end
95+
96+
# Private Functions
97+
98+
defp schedule_trim do
99+
Process.send_after(self(), :trim, @trim_interval)
100+
end
101+
102+
defp perform_trim(state) do
103+
case trim_log_file(state.log_file) do
104+
{:ok, :trimmed, lines_removed} ->
105+
Logger.debug("LogTrimmer: Trimmed #{lines_removed} lines from #{state.log_file}")
106+
%{state | last_trim: DateTime.utc_now(), trim_count: state.trim_count + 1}
107+
108+
{:ok, :no_trim_needed} ->
109+
%{state | last_trim: DateTime.utc_now()}
110+
111+
{:error, reason} ->
112+
Logger.warning("LogTrimmer: Failed to trim #{state.log_file}: #{reason}")
113+
state
114+
end
115+
end
116+
117+
defp trim_log_file(log_file) do
118+
try do
119+
if File.exists?(log_file) do
120+
content = File.read!(log_file)
121+
lines = String.split(content, "\n")
122+
line_count = length(lines)
123+
124+
if line_count > @max_log_lines do
125+
# Keep only the last @max_log_lines lines
126+
trimmed_lines = lines |> Enum.take(-@max_log_lines)
127+
trimmed_content = Enum.join(trimmed_lines, "\n")
128+
129+
# Write to a temporary file and rename for atomic operation
130+
temp_file = log_file <> ".tmp"
131+
File.write!(temp_file, trimmed_content)
132+
File.rename!(temp_file, log_file)
133+
134+
lines_removed = line_count - @max_log_lines
135+
{:ok, :trimmed, lines_removed}
136+
else
137+
{:ok, :no_trim_needed}
138+
end
139+
else
140+
{:ok, :no_trim_needed}
141+
end
142+
rescue
143+
error ->
144+
{:error, Exception.message(error)}
145+
end
146+
end
147+
end

lib/mix/tasks/restart.ex

Lines changed: 14 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ defmodule Mix.Tasks.Restart do
33
Restarts the Phoenix server in the background with output redirected to geo.log.
44
55
This task will:
6-
1. Stop any running Phoenix server
6+
1. Stop any running Phoenix server and log trimmer
77
2. Start the server in the background
88
3. Redirect both stdout and stderr to geo.log
9-
4. Start a background process to periodically trim the log file
9+
4. Start a background LogTrimmer process to periodically trim the log file
1010
1111
## Examples
1212
@@ -25,7 +25,7 @@ defmodule Mix.Tasks.Restart do
2525
def run(_args) do
2626
log_file = "geo.log"
2727

28-
# Stop existing server using the geo.stop task
28+
# Stop existing server and log trimmer using the geo.stop task
2929
Mix.Tasks.Stop.run([])
3030

3131
# Start server in background with output redirection
@@ -36,7 +36,7 @@ defmodule Mix.Tasks.Restart do
3636
# Create or truncate the log file
3737
File.write!(log_file, "")
3838

39-
# Start log trimming process in background
39+
# Start log trimming process
4040
start_log_trimmer(log_file)
4141

4242
# Use System.cmd with a shell command to properly background the process
@@ -82,62 +82,19 @@ defmodule Mix.Tasks.Restart do
8282
end
8383

8484
defp start_log_trimmer(log_file) do
85-
# Create a temporary script file for the log trimmer
86-
script_content = """
87-
#!/usr/bin/env elixir
85+
# Stop any existing log trimmer first
86+
Geo.LogTrimmer.stop()
8887

89-
defmodule LogTrimmer do
90-
@trim_interval #{@trim_interval}
91-
@max_log_lines #{@max_log_lines}
88+
# Start the LogTrimmer GenServer
89+
case Geo.LogTrimmer.start_link(log_file) do
90+
{:ok, _pid} ->
91+
Mix.shell().info("Log trimmer started successfully")
9292

93-
def run(log_file) do
94-
trim_log_periodically(log_file)
95-
end
93+
{:error, {:already_started, _pid}} ->
94+
Mix.shell().info("Log trimmer already running")
9695

97-
defp trim_log_periodically(log_file) do
98-
Process.sleep(@trim_interval)
99-
100-
try do
101-
trim_log_file(log_file)
102-
rescue
103-
_ -> :ok
104-
end
105-
106-
trim_log_periodically(log_file)
107-
end
108-
109-
defp trim_log_file(log_file) do
110-
if File.exists?(log_file) do
111-
content = File.read!(log_file)
112-
lines = String.split(content, "\\n")
113-
114-
if length(lines) > @max_log_lines do
115-
# Keep only the last @max_log_lines lines
116-
trimmed_lines = lines |> Enum.take(-@max_log_lines)
117-
trimmed_content = Enum.join(trimmed_lines, "\\n")
118-
119-
# Write to a temporary file and rename for atomic operation
120-
temp_file = log_file <> ".tmp"
121-
File.write!(temp_file, trimmed_content)
122-
File.rename!(temp_file, log_file)
123-
end
124-
end
125-
end
96+
{:error, reason} ->
97+
Mix.shell().error("Failed to start log trimmer: #{inspect(reason)}")
12698
end
127-
128-
LogTrimmer.run("#{log_file}")
129-
"""
130-
131-
script_file = "log_trimmer_#{System.system_time(:millisecond)}.exs"
132-
File.write!(script_file, script_content)
133-
134-
# Start the log trimmer in the background
135-
System.cmd("sh", ["-c", "nohup elixir #{script_file} > /dev/null 2>&1 &"])
136-
137-
# Clean up the script file after a moment
138-
spawn(fn ->
139-
Process.sleep(5000)
140-
File.rm(script_file)
141-
end)
14299
end
143100
end

lib/mix/tasks/stop.ex

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
defmodule Mix.Tasks.Stop do
22
@moduledoc """
3-
Stops the server if it's running.
3+
Stops the server and log trimmer if they're running.
44
5-
This task will find and stop any running server process.
5+
This task will find and stop any running server process and log trimmer.
66
77
## Examples
88
@@ -15,6 +15,12 @@ defmodule Mix.Tasks.Stop do
1515

1616
@impl Mix.Task
1717
def run(_args) do
18+
# Stop log trimmer first
19+
stop_log_trimmer()
20+
21+
# Stop orphaned log trimmer processes
22+
stop_orphaned_log_trimmers()
23+
1824
# Function to find the server PID
1925
find_phoenix_pid = fn ->
2026
case System.cmd("pgrep", ["-f", "beam.smp.*mix phx.server"], stderr_to_stdout: true) do
@@ -45,7 +51,7 @@ defmodule Mix.Tasks.Stop do
4551
# Graceful shutdown first
4652
case System.cmd("kill", ["-TERM", pid], stderr_to_stdout: true) do
4753
{_, 0} ->
48-
wait_for_shutdown(pid, 3)
54+
wait_for_shutdown(pid, 5)
4955

5056
# Check if still running and force kill if necessary
5157
case System.cmd("kill", ["-0", pid], stderr_to_stdout: true) do
@@ -66,6 +72,35 @@ defmodule Mix.Tasks.Stop do
6672
end
6773
end
6874

75+
defp stop_log_trimmer do
76+
try do
77+
case Geo.LogTrimmer.stop() do
78+
:ok ->
79+
Mix.shell().info("Log trimmer stopped.")
80+
_ ->
81+
# Already stopped or not running
82+
nil
83+
end
84+
rescue
85+
_ ->
86+
# LogTrimmer module might not be loaded in some contexts
87+
nil
88+
end
89+
end
90+
91+
# Helper function to stop orphaned log trimmer processes
92+
defp stop_orphaned_log_trimmers do
93+
{output, _} = System.cmd("pgrep", ["-af", "log_trimmer_.*\\.exs"], stderr_to_stdout: true)
94+
output
95+
|> String.split("\n")
96+
|> Enum.reject(&(&1 == ""))
97+
|> Enum.each(fn line ->
98+
[pid | _] = String.split(line)
99+
Mix.shell().info("Killing orphaned log trimmer with PID #{pid}")
100+
System.cmd("kill", ["-TERM", pid])
101+
end)
102+
end
103+
69104
# Helper function to wait for process to stop
70105
defp wait_for_shutdown(pid, seconds_remaining) when seconds_remaining > 0 do
71106
case System.cmd("kill", ["-0", pid], stderr_to_stdout: true) do

log_trimmer_1751842644438.exs

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)