ℹ️ Select 'Choose Exercise', or randomize 'Next Random Exercise' in selected language.

Choose Exercise:
Timer 00:00
WPM --
Score --
Acc --
Correct chars --

Erlang Distributed Coordination with Distributed Erlang

Erlang

Goal -- WPM

Ready
Exercise Algorithm Area
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.
Algorithm description viewbox

Erlang Distributed Coordination with Distributed Erlang

Algorithm description:

This Erlang code implements a distributed lock manager. It uses Erlang's built-in distribution capabilities (`rpc`) to allow processes on different nodes to acquire and release locks for shared resources. A central lock server process on each node manages the locks for resources residing on that node. When a lock is requested, the server checks if it's available; if not, the requesting process is added to a wait queue. If a node holding locks goes down, the system attempts to release those locks to prevent deadlocks. This pattern is fundamental for coordinating access to shared resources in distributed Erlang applications.

Algorithm explanation:

The `dist_lock` module provides a distributed lock manager. The `start/0` function spawns a `lock_server_loop/1` process on the current node, which maintains a map of resources to their lock status (holder PID and wait queue). The `acquire_lock/2` function uses `rpc:call` to send a request to the lock server on a specified `Node`. The `lock_server_loop` handles `acquire` messages: if the resource is free, it grants the lock; otherwise, it adds the requester to the wait queue. `release_lock/2` removes the lock and grants it to the next in line if any. `is_locked/1` checks the lock status. A `node_down` message handler is included to attempt cleanup if a node disconnects, releasing locks held by processes on that node. The `handle_lock_request`, `handle_release_request`, and `handle_is_locked_request` functions act as RPC wrappers, sending messages to the local `lock_server_loop` and waiting for replies. Time complexity for lock acquisition/release is O(1) on average if the resource is free or the next in queue is readily available, but can be O(N) in the worst case where N is the number of processes waiting for a lock on a resource. Space complexity is O(R*W) where R is the number of resources and W is the maximum number of waiting processes per resource.

Pseudocode:

Lock Server Process (per node):
  State: Map of Resource -> {HolderPid, WaitQueue}.
  Receive 'acquire' message (RequesterPid, Resource):
    If Resource not in state:
      Grant lock to RequesterPid, add to state.
      Notify RequesterPid.
    Else if RequesterPid already holds lock:
      Notify RequesterPid (already held).
    Else:
      Add RequesterPid to WaitQueue for Resource.
      Update state.
  Receive 'release' message (ReleaserPid, Resource):
    If Resource not in state or ReleaserPid is not HolderPid:
      Notify ReleaserPid (not held).
    Else:
      Remove lock from state.
      If WaitQueue is not empty:
        Dequeue NextPid.
        Grant lock to NextPid.
        Notify NextPid.
  Receive 'is_locked' message (Resource, RequesterPid):
    Check if Resource is in state.
    Notify RequesterPid with result.
  Receive 'node_down' message (DownNode):
    Iterate through state:
      If a lock is held by a process on DownNode:
        Release the lock and grant to the next in queue if any.

RPC Functions (client side):
  acquire_lock(Node, Resource):
    Send RPC 'acquire' to Node's lock server.
    Wait for reply (granted, already_held, error, timeout).
  release_lock(Node, Resource):
    Send RPC 'release' to Node's lock server.
    Wait for reply (ok, not_held, error, timeout).
  is_locked(Node, Resource):
    Send RPC 'is_locked' to Node's lock server.
    Wait for reply (boolean, error, timeout).