I am trying to add a record to :mnesia for a random session id string. The idea is if the session id already exists in mnesia, it should generate a new random id and try again. We should try n number of times.
I can do this with the following test code based off the suggestions here:
def add_session(user_name) do
write_session_stream = fn ->
countdown = Stream.unfold(5, fn
0 ->
IO.puts("FAILED TO ADD SESSION")
nil # Stop when we reach 0
n ->
session_id = RandomString.generate(10);
utc_now = DateTime.utc_now();
start_date_string = DateTime.to_iso8601(utc_now)
existing_id_list = :mnesia.read({AuthSession, session_id}) # check if session id exists
if length(existing_id_list) > 0 do
IO.puts("SESSION ID NOT UNIQUE - TRY AGAIN");
{n, n - 1} # Emit n and prepare for the next state
else
#DOESN'T EXIST
IO.puts("OKAY: ID DOESN'T EXIST " <> inspect(existing_id_list));
:mnesia.write({AuthSession, session_id, user_name, start_date_string})
nil #end loop
end
end)
Stream.run(countdown);
end
:mnesia.transaction(write_session_stream)
end
This works for adding the session data to mnesia and trying maximum n times. But I am not able to get out of it the value of the entry that was written as a return from the mnesia addition.
ie. I can’t return out: {session_id, user_name, date_string}
It instead returns {:atomic, :ok} which is useless.
This is presumably because I am returning nil from the countdown and the transaction returns Stream.run(countdown); which is just :ok. So that is all I can get back.
This is something that would be very simple in any other programming language. How can I do this in Elixir? Thanks for any help.
So I need to return what I want manually in there.
I re-wrote it as two functions recursively and this seems to work as say add_session_loop(user, 5).
def add_session_loop(user_name, num_try)do
if (num_try>0)do
result = add_session_once(user_name)
case (result)do
{:atomic, nil} ->
add_session_loop(user_name, num_try - 1)
_->
result
end
else
nil
end
end
def add_session_once(user_name) do
write_session = fn ->
session_id = RandomString.generate(10);
utc_now = DateTime.utc_now();
start_date_string = DateTime.to_iso8601(utc_now)
existing_id_list = :mnesia.read({AuthSession, session_id})
result = if length(existing_id_list) > 0 do
IO.puts("SESSION ID NOT UNIQUE - TRY AGAIN");
nil
else
#DOESN'T EXIST
IO.puts("OKAY: ID DOESN'T EXIST " <> inspect(existing_id_list));
:mnesia.write({AuthSession, session_id, user_name, start_date_string})
end
IO.puts("WRITE RESULT " <> inspect(result))
result = case result do
:ok ->
IO.puts("DONE OK")
{ session_id, user_name, start_date_string }
_->
nil
end
result;
end
:mnesia.transaction(write_session)
end
Why is write_session a function variable? Why not a normal separate function at the module level?
Also I’d rewrite add_session_loop like this:
def add_session_loop(user_name, 0), do: nil
def add_session_loop(user_name, num_try) do
result = add_session_once(user_name)
case result do
{:atomic, nil} ->
add_session_loop(user_name, num_try - 1)
_ ->
result
end
end
Seems you’ll benefit if you setup auto-formatting on file save in your IDE, by the way.
Thanks I’ll use your suggestions. I appreciate the pointers.
What is the difference? I tried moving it to a function but I still have to pass in an anonymous function to the :mnesia.transaction or it won’t work.
ChatGPT confirms this saying:
To pass a non-anonymous function into :mnesia.transaction, you typically need to wrap your function in an anonymous function. This is because :mnesia.transaction expects a function that takes no arguments.
So any difference is just stylistic at that point. I like having the logic stylistically right where it is being run.
Or is it faster to use :mnesia.transaction(fn -> MyModule.my_function(arg) end) and have the function separately?
I think the way I wrote it is likely faster. Could be more explicit to say :mnesia.transaction(fn -> [full function logic here] end) all at once but the compiler I presume will figure that out. No point in splitting to two functions except style if it is only ever run as one combined function.
Unfortunately that I can’t do. I am using VS Code with the Elixir LS add-on. If you turn on auto-formatting it forces 2 space tab indentations on you (rather than 4 space tabs), and that is not something I can survive.
// Note: While it is possible to override this in your VSCode configuration, the Elixir Formatter
// does not support a configurable tab size, so if you override this then you should not use the
// formatter.
"editor.tabSize": 2,