1-module(dist_lock).
2
3-export([start/0, stop/0, acquire_lock/2, release_lock/2, is_locked/1]).
4
5-define(LOCK_SERVER, dist_lock_server).
6
7%% Public API
8start() ->
9%% Start the lock server on each node that needs to participate.
10%% Ensure nodes are connected using erl -name or erl -sname.
11case whereis(?LOCK_SERVER) of
12undefined ->
13spawn(?MODULE, lock_server_loop, [#{}]), % Initial state: empty map
14ok;
15_ ->
16ok % Already started
17end.
18
19stop() ->
20%% In a real system, you'd need a way to signal all nodes to stop.
21%% For this example, we assume it's run on a single node or managed externally.
22exit(?LOCK_SERVER, normal).
23
24acquire_lock(Node, Resource) when is_atom(Node) ->
25%% Send an RPC request to the lock server on the target node.
26%% The server will handle the logic of granting or queuing the lock.
27rpc:call(Node, ?MODULE, handle_lock_request, [self(), Resource]).
28
29release_lock(Node, Resource) when is_atom(Node) ->
30%% Send an RPC request to release the lock.
31rpc:call(Node, ?MODULE, handle_release_request, [self(), Resource]).
32
33is_locked(Node, Resource) when is_atom(Node) ->
34%% Check if a resource is locked.
35rpc:call(Node, ?MODULE, handle_is_locked_request, [Resource]).
36
37%% Lock Server Loop
38lock_server_loop(State) when is_map(State) ->
39receive
40{'$gen_cast', {acquire, RequesterPid, Resource}} ->
41%% Check if resource is already locked
42case maps:get(Resource, State, undefined) of
43undefined -> % Not locked
44NewState = maps_put(Resource, {RequesterPid, []}, State), % Grant lock, empty queue
45RequesterPid ! {lock_granted, Resource}, % Notify requester
46lock_server_loop(NewState);
47{HolderPid, WaitQueue} when HolderPid == RequesterPid ->
48%% Already holds the lock, do nothing or error
49RequesterPid ! {lock_already_held, Resource},
50lock_server_loop(State);
51{HolderPid, WaitQueue} -> % Locked by someone else
52NewWaitQueue = WaitQueue ++ [RequesterPid],
53NewState = maps_put(Resource, {HolderPid, NewWaitQueue}, State),
54lock_server_loop(NewState)
55end;
56{'$gen_cast', {release, ReleaserPid, Resource}} ->
57%% Check if the releaser actually holds the lock
58case maps:get(Resource, State, undefined) of
59undefined ->
60%% Trying to release a lock that doesn't exist or isn't held by this process
61ReleaserPid ! {lock_not_held, Resource},
62lock_server_loop(State);
63{HolderPid, WaitQueue} when HolderPid == ReleaserPid ->
64%% Grant lock to the next in queue if any
65case WaitQueue of
66[] -> % No one waiting
67NewState = maps_delete(Resource, State),
68lock_server_loop(NewState);
69[NextPid | RestQueue] -> % Someone waiting
70NewState = maps_put(Resource, {NextPid, RestQueue}, State),
71NextPid ! {lock_granted, Resource}, % Notify next requester
72lock_server_loop(NewState)
73end;
74{HolderPid, WaitQueue} ->
75%% Trying to release a lock not held by this process
76ReleaserPid ! {lock_not_held, Resource},
77lock_server_loop(State)
78end;
79{'$gen_cast', {is_locked, Resource, RequesterPid}} ->
80IsLocked = maps:is_key(Resource, State),
81RequesterPid ! {is_locked_reply, Resource, IsLocked},
82lock_server_loop(State);
83{'$gen_cast', {node_down, DownNode}} ->
84%% Handle node down by releasing any locks held by processes on that node.
85%% This is a simplified approach; a more robust solution would involve
86%% tracking which process owns which lock and potentially re-assigning.
87NewState = maps:foldl(fun(Resource, {HolderPid, WaitQueue}, AccState) ->
88case HolderPid of
89Pid when is_pid(Pid) andalso node(Pid) == DownNode ->
90%% Grant lock to next in queue if any
91case WaitQueue of
92[] -> maps_delete(Resource, AccState);
93[NextPid | RestQueue] ->
94maps_put(Resource, {NextPid, RestQueue}, AccState)
95end;
96_ -> AccState % Lock held by a process on another node
97end
98end, State, State),
99lock_server_loop(NewState);
100%% Handle termination signal
101{'EXIT', _, _} ->
102exit(normal)
103end.
104
105%% RPC Handler Functions (executed on the node where the lock server is running)
106handle_lock_request(RequesterPid, Resource) ->
107%% Send a cast message to the lock server loop.
108%% The response will come back via the RequesterPid's mailbox.
109self() ! {'$gen_cast', {acquire, RequesterPid, Resource}},
110%% Wait for the response from the lock server loop.
111receive
112{lock_granted, Res} when Res == Resource -> {ok, Resource};
113{lock_already_held, Res} when Res == Resource -> {error, lock_already_held};
114{error, Reason} -> {error, Reason}
115after 5000 -> {error, timeout} % Timeout for lock acquisition
116end.
117
118handle_release_request(ReleaserPid, Resource) ->
119self() ! {'$gen_cast', {release, ReleaserPid, Resource}},
120receive
121{lock_not_held, Res} when Res == Resource -> {error, lock_not_held};
122{ok, Resource} -> {ok, Resource}
123after 5000 -> {error, timeout}
124end.
125
126handle_is_locked_request(Resource) ->
127self() ! {'$gen_cast', {is_locked, Resource, self()}},
128receive
129{is_locked_reply, Res, IsLocked} when Res == Resource -> IsLocked
130after 5000 -> {error, timeout}
131end.