Rust
,actix web
,tokio
for runtime,sqlx
to interact with dbDocker
for runningpostgres
andredis
containersPostgres
for primary dbRedis
for caching
The project comes with some make *
commands. To get started, we should first boot up the database and redis.
This can be done using the following commands.
make db-init
make redis-init
Running (development mode)
make run
# we can also watch for changes in the codebase by running
# make watch
The service exposes 1 health check
endpoint and 2 other endpoints /shorten
and /visit
.
The endpoint requires url
as data, which needs to be a valid url. it is a post method. Upon given a valid url, it will return
a unique key with the base url (configured). The base url has been configured for tier.app
.
Request
curl --location 'http://localhost:1234/shorten' \
--header 'Content-Type: application/json' \
--data '{
"url" : "https://apple.com"
}'
Response
{
"short_url": "tier.app/DaHvis_"
}
Once we have our key, which is the unique id after tier.app/
, we can make a request to /visit/${key}
url
for it to be validated and redirected to the original url.
Request
curl --location 'http://localhost:1234/visit/PL9B8g5'
If the key is valid and either present in cache or db, the user would be redirect to the original url. If not, it will return an error response with a message.
/health-check
endpoint returns a status code of the connected db and cache.
Request
curl --location 'http://localhost:1234/health-check'
Response
{
"cache_is_alive": true,
"db_is_alive": true,
"reporting_time": "2023-09-17 05:00:12.967406 UTC"
}
Postman collection has been provided inside docs/tier.postman_collection.json.
The project exposes and wraps everying related to url shortener inside src/url_shortener
directory (apart from config & configration). This way the code can be exported as a package by just taking the code from src/url_shortener
and also
can be served as a service that runs on it it's own by simply running src/main.rs.
src/main.rs only enables logging, reads the config file and calls the UrlShortenerService
and HttpServer
.
HttpServer
is currently a blank struct, for simplity and scope of this challenge. In production, theHttpServer
can be initiated with it's own configuration (eg: timeouts, tls serving etc).
The UrlShortenerService depends on a database provider and a caching provider. Both have been exposed using traits, therefore it is not bound to postgres
or redis
or anything. Any database can implement DataStore and any cache can
implement CacheStore for the service to work.
By default, a postgres and redis driver have been provided.
The service lets the database to handle it's own logic. Eg: which column to query, the service is not concerned about that, and is
handed of the database via traits. So for some reason if a database wishes to call the original_url
as actual_url
, it can. Same for the caching.
The service itself exposes some public method, where in those traits are called, along with some logic to provide the desired service (shortening urls).
The generate_unique_key function takes an input string, applies a cryptographic hash, combines it with a random value (salt), encodes it in base64, and then truncates it to a specified length. This process ensures that the generated key is unique for each input and is of a specified length.
I am trauncating the result of the final value. It can create a colluision theorytically. Though salt has been added. A collusion checking mechanism using a (pseudo) rate limiter has been implemented.
This apporach creates multiple quries to the database. A better apporach would be to pregenerate the keys, putting them in hashmap / similar datastructure and taking them from that set. Sort of like a pool of values, when we need a key, we take from theat pool and mark it as
used
/ similar to indicate that key is no longer valid. The pool should be thread safe in case of multiple client connections trying to take a key at the same time.
There are some known issues, they were not fixed due to the lack of time. I'm listing them here,
- More tests can be added, only integration tests are not present.
- The approach of integration test can be improved. Now, on every test iteration it create a database but does not database.
- Benchmarks tests are not present.
- Running tests also require docker, the db and caching drivers can be mocked.
- Resulting in lots of database. This can cause a problem if tests are frequently ran and can hangup file descriptors.
- No cache manager/database manager was provided, right now in the main file it is hardcoded which driver we are using. But we can use something similar to this (different project).
- Wanted to take an approach to registering a key-value to cache in a background tasks but due to the lack of time, didn't get the chance to work with background tasks.
- Error handling was done pretty simply with Result<T,String> or Result<T, sqlx::Error>, crates liek
anyhow::error
could've helped improve this approach. - Right now, the caching is (also) acting as a secondary database, we might not need to have all the URLs in cache, we can approach LRU or LFU caching based on the requirement (to come).
- Actix handles
SIGTERM
andSIGKILL
, I did not handle it to do something particular. - API versioning was left out.
- Reading sensitive data from / for environment is not plain text, eg: database passwords, this can be imrpvoed by
secrecy::Secret
crate, that keep logs / tracing to collect sentivite data. - SQL constrains were based on a simple assumtion.
- The
visits
table acts as a log, so we can aggregate in the future if needed. Storing the simplest unit of data. - SQL builder can help in the db drivers level.