A Rails API app for generating and rating posts using high-throughput concurrent seeds.
No any auth logic. No frontend UI. No any CI/CD customization.
- Ruby 3.4.4
- PostgreSQL
- Rspec
-
Install dependencies:
bundle install
-
Create, init DB
bundle exec rails db:create bundle exec rails db:migrate
-
Running Rspec and Rubocop
make check
-
Running seeds with Makefile
make db_seed
-
Running seeds manually you need to start rails puma in dev mode before
bundle exec rails sor in cluster mode (gives 4-5x performance boost)
WEB_CONCURRENCY=8 RAILS_MAX_THREADS=10 bundle exec puma -C config/puma.rbthan open new tab in terminal and run seeds as usual
bundle exec rails db:seed
Creates a new post. If the user does not exist, it will be created.
curl -X POST http://localhost:3000/api/v1/posts \
-H "Content-Type: application/json" \
-d '{
"title": "My first post",
"body": "Hello from curl",
"login": "john_doe",
"ip": "123.123.123.123"
}'
Success response example, code 201
{
"data": {
"post": {
"id": 1,
"title": "My first post",
"body": "Hello from curl",
"ip": "123.123.123.123",
"user_id": 1,
"created_at": "...",
"updated_at": "..."
},
"user": {
"id": 1,
"login": "john_doe",
"created_at": "...",
"updated_at": "..."
}
}
}Response with errors, code 422
{
"errors": [
"Login can't be blank","Login is too short (minimum is 4 characters)"
]
}Not valid IP, code 422
{
"errors": [
"Ip must be a valid IP address"
]
}Allows a user to rate a post once.
curl -X POST http://localhost:3000/api/v1/posts/1/ratings \
-H "Content-Type: application/json" \
-d '{
"user_id": 1,
"value": 4
}'Success response example, code 200
{
"data": {
"average_rating":4.0
}
}Response with errors, code 422
{
"errors": [
"Rating must be between 1 and 5"
]
}Returns the top-rated posts, sorted by average rating in descending order.
curl -X GET "http://localhost:3000/api/v1/posts/top?page=4&limit=7
Query Parameters:
-
page (optional, integer): Pagination offset
-
limit (optional, integer): Number of top posts to return. Default is 50, max is 250 records
{
"data": [
{
"id": 946,
"title": "Labore aut nemo.",
"body": "Quis sunt sed. Dolorum animi veritatis."
},
{
"id": 1560,
"title": "Quaerat consequuntur et.",
"body": "Nisi laboriosam similique. Voluptatem iste vitae."
},
{
"id": 764,
"title": "Veritatis nesciunt quisquam.",
"body": "Molestiae voluptatum et. Quaerat facere labore."
},
{
"id": 113,
"title": "Et laborum quis.",
"body": "Pariatur perferendis laboriosam. Accusantium maxime ipsa."
},
],
"page_info": {
"page": 4,
"limit": 4,
"has_next_page": true
}
}Response with error:
{"errors":["Limit cannot exceed 250"]}Returns a list of IP addresses that were used by multiple users.
curl -X GET "http://localhost:3000/api/v1/posts/top?page=4&limit=7
Query Parameters:
-
page (optional, integer): Pagination offset
-
limit (optional, integer): Number of records to return. Default is 50, max is 100 records
{
"data": [
{
"ip": "108.92.74.37",
"logins": [
"arnold",
"candy",
"clair",
"delilah"
]
},
{
"ip": "11.203.206.75",
"logins": [
"carlee",
"carrol",
"cathi",
"debbra",
"dulce"
]
}
],
"page_info": {
"page": 2,
"limit": 2,
"has_next_page": true
}
}make check
Running RuboCop...
Inspecting 34 files
..................................
34 files inspected, no offenses detected
Running RSpec tests...
Randomized with seed 64453
Post
associations
belongs to user
has many ratings and destroys them when post is destroyed
validations
is valid with valid attributes
is invalid if title is too short
is invalid if body is too short
is invalid with IPv6 address
is invalid without a title
is invalid if body is too long
is invalid without an ip
is invalid with IP-like garbage
is invalid if title is too long
GET /api/v1/posts/top
returns error when limits greater than 250 posts
returns top N posts ordered by average rating
POST /api/v1/posts/:post_id/ratings
rating a post
returns error if user already rated the post
creates a rating and returns average
returns error when rating is invalid
returns error when user not found
returns error when post not found
Rating
validations
is invalid without value
is valid with value between 1 and 5
is invalid if the same user rates the same post twice
is invalid if value is greater than 5
is invalid if value is less than 1
GET /api/v1/posts/shared_ips
returns next_page: false when on last page
returns shared IPs with multiple authors
respects pagination with limit and next_page
returns error if limit exceeds max
User
is valid with a login between 4 and 5 characters
is invalid if login is too short
is invalid if login is too long
is invalid without a login
PaginationConcern
#pagination_params
when limit is negative
defaults to DEFAULT_LIMIT
when limit exceeds max_limit
raises PaginationLimitExceeded
when valid params provided
returns them as is
when no params are provided
returns default page and limit
when page is invalid
defaults to page 1
#paginate_with_next_page
returns next_page false when no more results
returns correct slice and next_page true if there is more
POST /api/v1/posts
returns error for invalid post (missing title)
creates a new user and post
uses existing user if login matches
returns error for invalid user (missing login)
Finished in 0.91493 seconds (files took 0.65318 seconds to load)
42 examples, 0 failures
Randomized with seed 64453
After a brief investigation, I decided to use Faraday within Ruby runtime for sending HTTP requests. In my setup (AMD 8 cores x86-64, 16 GB, SSD, Fedora), it consistently outperformed parallel curl via xargs, achieving nearly 2x better performance in terms of request throughput and overall execution time.
- Uses Faraday gem for sending requests in multiple Ruby threads.
- Puma is launched in cluster mode with tuned thread/concurrency settings.
seeds.rboptimized to avoid duplicate users and minimize transaction contention.- Uses Oj for fast JSON parsing.
- DB connection pool parameter was tuned
Script generates at least 200k posts and about 100 users with 70 unique IP addresses. 75% of posts have ratings. The script uses API controllers and actions for creating records in DB.
There are several ways to further develop the application:
-
Add rate limiting and throttling to protect the API from overload and abuse.
-
Move rating average calculations to a background job for better performance and scalability.
-
Add a bulk insert endpoint to efficiently handle large batches of incoming records.
