This project is a solution for a simple coding exercise. It's name, "Friendly", is a random choice but not designed to be an opinion on naming projects by codeword vs purpose.
Typically in an internal application I would link to background and desired business outcomes. I am not linking here out of coyness; this is a public repo, while the exercise is proprietary.
Briefly though:
- There is a single API endpoint.
- Calling that end point returns a JSON response with containing the timestamp of the previous call and a list of up to two users. The users are selected as having "points" above a particular randomly assigned value. That value, and every the users' points are updated once a minute to random values.
- If less than two users qualify, then less than two users are returned.
- That qualifying value, and the user's points are restricted to be in the range of 0 to 100 (inclusive)
- As a user's points must be above the qualifying value to be returned then no user with 0 points will ever be returned.
- When the qualifying value is occasionally randomly assigned to be 100, then no user can be returned.
It is set up for development. Actual deployment is out of scope.
I have provided some commentary on the design, and "exercise specification" in ./COMMENTARY.md
.
First clone this repo from Github and cd into the directory.
git clone git@github.com:paulanthonywilson/friendly.git
cd friendly
You will need installed Erlang, Elixir, and Postgresql to be installed.
Elixir and Erlang versions are set in .tool-versions
. As you are probably using asdf
you may need to asdf install
or edit the tool-versions
file. If you do run
with different Elixir or Erlang version and get warnings or errors, then that's on you though 😜.
Running mix setup
will get dependencies, create and migrate the development database, and seed the development database with 1,000,000 (1 million) entries in the users
table, each with zero points.
Inserting 1,000,000 users does take time: just over 30 seconds on my M2 Macbook Pro (8 cores).
mix setup
is idempotent in that if entries are found in the only table, users
then seeding will abort. You can override this by seeding seperately with some arguments.
To seed an extra million userse use the --force
argument.
mix run priv/repo/seeds.exs --force
To drop the existing million, and seed a different million use --delete-users
mix run priv/repo/seeds.exs --delete-users
Please do run the unit tests.
mix test
As per out-of-the-box Phoenix generated applications, on the first test run the test database will be silently created and migrated.
Credo
should have no suggestions. Running with --strict
will throw out a few things that I think are fine in their particular context.
mix credo
Dialyzer should show no errors or warnings.
mix dialyzer
If you like, you can run mix docs
and read this and other documentation at doc/readme.html
Run with iex -S mix phx.server
(or mix phx.server
if that's your thing). The server listens on localhost
(127.0.0.1), port 4000. You can navigate to http://localhost:4000 in your browser to see the returned JSON. (Aesthetes may prefer to curl localhost:4000 | jq
.)
If you query for the first time after freshly seeding, and within one minute of startup your result json will be like
{
"previous_query_timestamp": null,
"qualifying_users": []
}
As to qualify to be returned the user must have points greater than a random value that can not be less than zero, and all users have zero points then no users will qualify for the JSON. As there has been no previous query to the api since startup, then the "previous_query_timestamp" will be null
.
Querying again, within that first minute will show the time of the last query. Shortly before the time of writing this resulted in
{
"previous_query_timestamp": "2023-03-01T12:44:15.911222Z",
"qualifying_users": []
}
After one minute the JSON will probably (49 out of 50 chance) return two users and you will see something like
{
"previous_query_timestamp": "2023-03-01T12:44:31.636517Z",
"qualifying_users": [
{
"id": 6035861,
"points": 81
},
{
"id": 6035878,
"points": 97
}
]
}
There is no API way to see or manipulate the random value used to determine which users qualify to be returned. If you like you can hack it from the repl though (which is a good reason to use iex -S mix phx.server
rather than mix phx.server
). Bear in mind that you have a maximum of one minute to query after hacking that value before it is randomly updated.
iex> :sys.replace_state(PointsManager,&%{&1 | qualifying_points_floor: 100})
{
"previous_query_timestamp": "2023-03-01T12:54:10.954240Z",
"qualifying_users": []
}
No user can have points greater than 100, so none will be returned.
iex(6)> :sys.replace_state(PointsManager,&%{&1 | qualifying_points_floor: 99})
{
"previous_query_timestamp": "2023-03-01T13:01:22.467757Z",
"qualifying_users": [
{
"id": 6028463,
"points": 100
},
{
"id": 6028774,
"points": 100
}
]
}
Only users with 100 points will be returned; with 1,000,000 users there are bound to be around 10,000 maximum scoring users to choose from.
(The contents of .iex.exs
are responsible for the lack of namespacing in the iex
examples above.)
You can run in production mode by sourcing .prod_env
before iex -S mix phx.server
. The only reason I can think of doing that it is to look at the boiler plate 404 error, if you (say) curl http://localhost:4000/somewhere
, but the facility is there.