System design URL shortener: Spring Boot 4.1.1, base62 ids, Redis cache-aside, Redis Lua token bucket, 24 tests and live transcripts
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
$ curl -si -X POST /api/v1/urls -d '{"url":"https://ankurm.com/java-interview-preparation-guide/"}'
|
||||
HTTP/1.1 201
|
||||
X-RateLimit-Remaining: 4
|
||||
Location: http://localhost:8080/6MelkkB
|
||||
{"code":"6MelkkB","shortUrl":"http://localhost:8080/6MelkkB","expiresAt":null}
|
||||
|
||||
$ curl -si -X GET /6MelkkB
|
||||
HTTP/1.1 302
|
||||
Location: https://ankurm.com/java-interview-preparation-guide/
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
code = 7qqChen
|
||||
$ redis-cli EXISTS url:7qqChen # before the first redirect
|
||||
0
|
||||
--- after 1st redirect (cache miss, database read, cache filled)
|
||||
$ redis-cli GET url:7qqChen
|
||||
https://example.com/cache-demo
|
||||
$ redis-cli TTL url:7qqChen
|
||||
3600
|
||||
cache hits +0, misses +1
|
||||
--- after 9 more redirects
|
||||
cache hits +9, misses +1
|
||||
$ redis-cli GET clicks:7qqChen
|
||||
10
|
||||
$ curl -s http://127.0.0.1:8080/api/v1/urls/7qqChen/stats
|
||||
{"code":"7qqChen","longUrl":"https://example.com/cache-demo","clicks":10,...
|
||||
@@ -0,0 +1,13 @@
|
||||
$ curl -si -X GET /Zq9Zq9
|
||||
HTTP/1.1 404
|
||||
{"detail":"no such code: Zq9Zq9","instance":"/Zq9Zq9","status":404,"title":"Not Found"}
|
||||
|
||||
$ curl -si -X GET /Zq9Zq9
|
||||
HTTP/1.1 404
|
||||
{"detail":"no such code: Zq9Zq9","instance":"/Zq9Zq9","status":404,"title":"Not Found"}
|
||||
|
||||
cache misses: before=2 after-first-404=3 after-second-404=3
|
||||
$ redis-cli GET url:Zq9Zq9 # the negative-cache marker
|
||||
-
|
||||
$ redis-cli TTL url:Zq9Zq9
|
||||
60
|
||||
@@ -0,0 +1,21 @@
|
||||
$ curl -si -X POST /api/v1/urls -d '{"url":"https://example.com/one","alias":"demo-link"}'
|
||||
HTTP/1.1 201
|
||||
X-RateLimit-Remaining: 4
|
||||
Location: http://localhost:8080/demo-link
|
||||
{"code":"demo-link","shortUrl":"http://localhost:8080/demo-link","expiresAt":null}
|
||||
|
||||
$ curl -si -X POST /api/v1/urls -d '{"url":"https://example.com/two","alias":"demo-link"}'
|
||||
HTTP/1.1 409
|
||||
X-RateLimit-Remaining: 3
|
||||
{"detail":"alias already taken: demo-link","instance":"/api/v1/urls","status":409,"title":"Conflict"}
|
||||
|
||||
$ curl -si -X POST /api/v1/urls -d '{"url":"ftp://example.com/file"}'
|
||||
HTTP/1.1 400
|
||||
X-RateLimit-Remaining: 2
|
||||
{"detail":"only http and https URLs are allowed","instance":"/api/v1/urls","status":400,"title":"Bad Request"}
|
||||
|
||||
$ curl -si -X POST /api/v1/urls -d '{"url":"https://example.com","alias":"api"}'
|
||||
HTTP/1.1 400
|
||||
X-RateLimit-Remaining: 1
|
||||
{"detail":"alias must be 4-32 chars of A-Z a-z 0-9 _ -","instance":"/api/v1/urls","status":400,"title":"Bad Request"}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
capacity 5, refill 0.5 tokens/second (one every 2 s)
|
||||
request 1 -> HTTP/1.1 201 X-RateLimit-Remaining: 4
|
||||
request 2 -> HTTP/1.1 201 X-RateLimit-Remaining: 3
|
||||
request 3 -> HTTP/1.1 201 X-RateLimit-Remaining: 2
|
||||
request 4 -> HTTP/1.1 201 X-RateLimit-Remaining: 1
|
||||
request 5 -> HTTP/1.1 201 X-RateLimit-Remaining: 0
|
||||
request 6 -> HTTP/1.1 429 X-RateLimit-Remaining: 0 Retry-After: 2
|
||||
request 7 -> HTTP/1.1 429 X-RateLimit-Remaining: 0 Retry-After: 2
|
||||
$ redis-cli HGETALL rl:127.0.0.1
|
||||
tokens
|
||||
0.046000000000000082
|
||||
ts
|
||||
1792656760797
|
||||
--- sleeping 2.2 s
|
||||
request 8 -> HTTP/1.1 201 X-RateLimit-Remaining: 0
|
||||
--- redirects are not limited: 30 GETs in a row
|
||||
302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302 302
|
||||
@@ -0,0 +1,5 @@
|
||||
created ArYvqWu with ttlSeconds=2
|
||||
t=0s -> 302
|
||||
$ redis-cli TTL url:ArYvqWu # capped at the link's remaining life, not the 1 h default
|
||||
2
|
||||
t=3s -> 410
|
||||
@@ -0,0 +1,24 @@
|
||||
--- instance A: three links
|
||||
id | code | len
|
||||
----+---------+-----
|
||||
1 | 6MelkkB | 7
|
||||
2 | 7qqChen | 7
|
||||
3 | J4Wn4LP | 7
|
||||
(3 rows)
|
||||
|
||||
--- instance A stopped and restarted (ids 4..100 of its first block are never used)
|
||||
id | code | len
|
||||
-----+---------+-----
|
||||
1 | 6MelkkB | 7
|
||||
2 | 7qqChen | 7
|
||||
3 | J4Wn4LP | 7
|
||||
101 | IZKi2wO | 7
|
||||
(4 rows)
|
||||
|
||||
--- two instances running at once never overlap: B starts while A is alive
|
||||
id | code
|
||||
-----+---------
|
||||
102 | DDCfjej
|
||||
201 | Globq13
|
||||
(2 rows)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
code=7sWzEmdQLw length=10
|
||||
code=7sWzF1xlNQ length=10
|
||||
code=7sWzF7djyi length=10
|
||||
id | code
|
||||
--------------------+------------
|
||||
106666141551718400 | 7sWzEmdQLw
|
||||
106666141778210816 | 7sWzF1xlNQ
|
||||
106666141862096896 | 7sWzF7djyi
|
||||
(3 rows)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
PASS Base62Test.knownValues
|
||||
PASS Base62Test.roundTripsAcrossTheRange
|
||||
PASS Base62Test.rejectsGarbage
|
||||
PASS Base62Test.sevenCharactersCoverAboutThreePointFiveTrillion
|
||||
PASS CodeScramblerTest.noTwoIdsShareACodeAcrossTwoMillionConsecutiveIds
|
||||
PASS CodeScramblerTest.everyCodeIsExactlySevenCharacters
|
||||
PASS CodeScramblerTest.idsBeyondTheFortyBitDomainFallBackToPlainBase62
|
||||
PASS CodeScramblerTest.consecutiveIdsDoNotGiveConsecutiveCodes
|
||||
PASS RateLimitTest.burstOfFiveThenRejectedThenRefilled
|
||||
PASS RateLimitTest.redirectsAreNeverRateLimited
|
||||
PASS SnowflakeIdGeneratorTest.idsIncreaseWithinOneThread
|
||||
PASS SnowflakeIdGeneratorTest.base62CodeIsLongerThanForASequenceId
|
||||
PASS SnowflakeIdGeneratorTest.refusesToIssueIdsWhenTheClockGoesBackwards
|
||||
PASS SnowflakeIdGeneratorTest.idsAreUniqueAcrossEightThreads
|
||||
PASS SnowflakeIdGeneratorTest.nodeIdIsEncodedInTheId
|
||||
PASS SnowflakeIdGeneratorTest.rejectsOutOfRangeNodeId
|
||||
PASS SnowflakeProfileTest.snowflakeIdsGiveLongerCodes
|
||||
PASS UrlShortenerApiTest.sameLongUrlGetsAnIndependentCodeEachTime
|
||||
PASS UrlShortenerApiTest.customAliasWorksAndSecondUseIsAConflict
|
||||
PASS UrlShortenerApiTest.unknownCodeIs404AndTheMissIsNegativelyCached
|
||||
PASS UrlShortenerApiTest.createsAShortLinkAndRedirectsWith302
|
||||
PASS UrlShortenerApiTest.countsClicks
|
||||
PASS UrlShortenerApiTest.rejectsBadInput
|
||||
PASS UrlShortenerApiTest.expiredLinkIs410
|
||||
24 tests, 0 failures
|
||||
Reference in New Issue
Block a user