0Pricing
DSA Interview Prep · Pelajaran

Merancang Pembatas Laju dan Merancang Feed Twitter

Terapkan kerangka tersebut pada dua masalah desain kanonis: pembatasan laju token-bucket/sliding-window dan feed berita fan-out-on-write vs fan-out-on-read

Merancang Pembatas Laju dan Merancang Feed Twitter adalah pelajaran DSA Interview Prep gratis di CoddyKit. Ini adalah pelajaran 4 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar DSA Interview Prep, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Mengapa Pembatasan Laju Penting

Pembatasan laju mengendalikan jumlah permintaan yang dapat dibuat klien ke antarmuka pemrograman aplikasi dalam jangka waktu tertentu. Tanpanya, satu klien yang berperilaku buruk (atau serangan penolakan layanan terdistribusi) dapat memenuhi sumber daya peladen dan menurunkan kualitas layanan bagi semua pengguna. Pembatasan laju juga melindungi dari serangan tebak kata sandi, mencegah pengambilan data otomatis dari antarmuka pemrograman aplikasi, dan menegakkan penggunaan yang adil atas sumber daya bersama.

Tingkat penerapan pembatasan laju yang umum: per ID pengguna, per kunci antarmuka pemrograman aplikasi, per alamat IP, per titik akhir, atau gabungan beberapa tingkat tersebut. Batas yang umum: 100 permintaan per menit per pengguna, 1000 permintaan per jam per kunci antarmuka pemrograman aplikasi. Pembatas laju harus cepat (menambahkan overhead <1 milidetik) dan terdistribusi (konsisten di semua replika peladen aplikasi).

# Rate limiting scenarios
use_cases = [
    ('API authentication endpoint', '5 attempts per 15 min per IP', 'Brute-force protection'),
    ('Public search API',           '100 requests per minute per key', 'Fair use enforcement'),
    ('Email sending',               '50 emails per hour per user', 'Spam prevention'),
    ('Payment processing',          '10 transactions per second per account', 'Fraud prevention'),
    ('File upload',                 '5 uploads per minute per user', 'Resource quota'),
    ('Notification service',        '1000 pushes per second globally', 'Cost control'),
]
print(f'{'Endpoint/Feature':35s} {'Limit':40s} {'Reason'}')
print('-'*95)
for endpoint, limit, reason in use_cases:
    print(f'{endpoint:35s} {limit:40s} {reason}')

Algoritme Pembatasan Laju 1: Wadah Token

Algoritme wadah token mempertahankan wadah dengan kapasitas maksimum N token. Token ditambahkan pada laju tetap (misalnya, 10 per detik). Setiap permintaan menghabiskan satu token. Jika wadah kosong, permintaan ditolak. Jika jumlah token masih di bawah kapasitas, permintaan diterima dan token tersebut digunakan.

Wadah token memungkinkan lonjakan: jika tidak ada permintaan selama 5 detik, wadah terisi hingga N token, lalu N permintaan dapat masuk sekaligus. Algoritme ini sesuai untuk antarmuka pemrograman aplikasi yang sesekali menerima lonjakan. Kedua parameternya adalah kapasitas (ukuran lonjakan) dan laju pengisian ulang.

import time

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity       # max tokens (burst size)
        self.refill_rate = refill_rate # tokens added per second
        self.tokens = capacity         # start full
        self.last_refill = time.time()

    def allow(self):
        now = time.time()
        elapsed = now - self.last_refill
        # Refill tokens based on elapsed time
        self.tokens = min(self.capacity,
                          self.tokens + elapsed * self.refill_rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True    # request allowed
        return False       # rate limited

bucket = TokenBucket(capacity=5, refill_rate=2)  # 2 tokens/sec, burst=5
for i in range(8):
    allowed = bucket.allow()
    print(f'Request {i+1}: {"ALLOWED" if allowed else "REJECTED"} (tokens={bucket.tokens:.1f})')
    time.sleep(0.1)  # 0.1s between requests

Algoritme Pembatasan Laju 2: Catatan Jendela Bergeser

Catatan jendela bergeser menyimpan stempel waktu untuk setiap permintaan dalam himpunan terurut. Untuk setiap permintaan baru, hapus stempel waktu yang lebih lama dari awal jendela, lalu periksa apakah jumlah stempel waktu yang tersisa berada di bawah batas. Jika ya, tambahkan stempel waktu saat ini dan allow; jika tidak, tolak.

Metode ini tepat—metode ini menghitung persis jumlah permintaan yang terjadi dalam N detik terakhir. Komprominya adalah penggunaan memori yang tinggi (satu entri untuk setiap permintaan bagi setiap pengguna). Untuk batas 1000 permintaan per menit dengan 100 ribu pengguna, kondisi terburuknya adalah 100 juta entri catatan. Metode ini tidak sesuai untuk lalu lintas yang sangat tinggi kecuali digabungkan dengan pemartisian.

import time
from collections import deque

class SlidingWindowLog:
    def __init__(self, limit, window_seconds):
        self.limit = limit
        self.window = window_seconds
        self.logs = {}     # user_id -> deque of timestamps

    def allow(self, user_id):
        now = time.time()
        if user_id not in self.logs:
            self.logs[user_id] = deque()
        log = self.logs[user_id]
        window_start = now - self.window
        # Remove expired timestamps
        while log and log[0] <= window_start:
            log.popleft()
        # Check limit
        if len(log) < self.limit:
            log.append(now)
            return True
        return False

limiter = SlidingWindowLog(limit=3, window_seconds=10)
for i in range(5):
    allowed = limiter.allow('user123')
    print(f'Request {i+1}: {"ALLOWED" if allowed else "REJECTED"}')
    time.sleep(0.5)

Algoritme Pembatasan Laju 3: Penghitung Jendela Bergeser

Penghitung jendela bergeser memperkirakan jendela bergeser menggunakan dua wadah—menit saat ini dan menit sebelumnya—dengan bobot berdasarkan seberapa jauh kita berada di dalam menit saat ini. Metode ini mengurangi penggunaan memori dari O(requests) menjadi O(1) per pengguna, sekaligus memberikan perkiraan yang mendekati hitungan jendela bergeser yang tepat.

Rumus: estimated_count = prev_count × (1 - fraction_of_window_elapsed) + curr_count. Jika perkiraan hitungan ini melebihi batas, permintaan ditolak. Algoritme ini digunakan oleh Cloudflare dan Kong dalam skala besar karena penggunaan memorinya O(1) per pengguna dan tingkat keakuratannya tinggi.

import time
import math

class SlidingWindowCounter:
    def __init__(self, limit, window_seconds=60):
        self.limit = limit
        self.window = window_seconds
        self.buckets = {}   # user_id -> {prev_count, curr_count, curr_window_start}

    def allow(self, user_id):
        now = time.time()
        window_start = int(now // self.window) * self.window

        if user_id not in self.buckets or self.buckets[user_id]['window'] < window_start - self.window:
            self.buckets[user_id] = {'prev': 0, 'curr': 0, 'window': window_start}
        elif self.buckets[user_id]['window'] < window_start:
            self.buckets[user_id] = {'prev': self.buckets[user_id]['curr'], 'curr': 0, 'window': window_start}

        b = self.buckets[user_id]
        fraction = (now - window_start) / self.window
        estimated = b['prev'] * (1 - fraction) + b['curr']

        if estimated < self.limit:
            b['curr'] += 1
            return True
        return False

limiter = SlidingWindowCounter(limit=5, window_seconds=10)
for i in range(7):
    print(f'Request {i+1}: {"OK" if limiter.allow("user1") else "RATE LIMITED"}')
    time.sleep(0.3)

Pembatasan Laju Terdistribusi dengan Redis

Dalam sistem terdistribusi dengan beberapa peladen aplikasi, pembatasan laju harus dipusatkan—jika tidak, setiap peladen akan menghitung jumlahnya sendiri dan batas efektifnya menjadi berlipat ganda sesuai jumlah peladen. Redis dengan operasi atomik adalah solusi standar: gunakan INCR dan EXPIRE untuk penghitung jendela tetap, atau ZADD dan ZCOUNT untuk catatan jendela bergeser.

Pendekatan skrip Lua membuat beberapa operasi Redis menjadi atomik dan mencegah kondisi balapan ketika dua peladen menambah jumlah secara bersamaan tepat di bawah batas. Redis memproses skrip Lua sebagai satu perintah sehingga menjamin keatomikan tanpa kunci terdistribusi.

# Distributed rate limiting with Redis (pseudocode / simulation)

# Fixed window counter using Redis INCR + EXPIRE
def redis_fixed_window(redis_client, user_id, limit, window_sec):
    key = f'rl:{user_id}:{int(time.time() // window_sec)}'
    count = redis_client.incr(key)          # atomic increment
    if count == 1:
        redis_client.expire(key, window_sec)  # set TTL on first request
    return count <= limit

# Sliding window with sorted set
def redis_sliding_window(redis_client, user_id, limit, window_sec):
    now = time.time()
    key = f'rl:{user_id}'
    # Remove old entries, count recent, add current
    # Atomic with Lua: multi-step operation
    lua_script = '''
    local key = KEYS[1]
    local now = ARGV[1]
    local window = ARGV[2]
    local limit = ARGV[3]
    redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
    local count = redis.call('ZCARD', key)
    if count < tonumber(limit) then
        redis.call('ZADD', key, now, now)
        redis.call('EXPIRE', key, window)
        return 1  -- allowed
    end
    return 0  -- rejected
    '''
    print('Redis Lua script ensures atomicity across ZREM + ZCARD + ZADD')

Merancang Umpan Twitter: Persyaratan

Mari kita merancang sistem umpan berita mirip Twitter. Persyaratan fungsional: pengguna dapat menerbitkan kiriman hingga 280 karakter, mengikuti pengguna lain, dan melihat umpan kiriman dari orang yang mereka ikuti, diurutkan berdasarkan waktu terbaru. Persyaratan nonfungsional: 300 juta pengguna aktif harian, 500 juta kiriman per hari, umpan harus dimuat dalam <2 detik, dengan rasio baca:tulis sekitar 100:1.

Perkiraan kapasitas: 500 juta kiriman per hari ÷ 86400 ≈ 5800 kiriman per detik. Pembacaan ≈ 580 ribu per detik. Setiap kiriman berukuran sekitar 300 byte; 500 juta × 300 byte = 150 GB penyimpanan kiriman baru per hari. Agregasi umpan adalah tantangan utama dalam rekayasa sistem.

# Twitter feed requirements and estimates
reqs = {
    'Functional': [
        'Post tweet (text, image, video)',
        'Follow/unfollow users',
        'View home feed (tweets from followees, newest first)',
        'View user timeline (all tweets by one user)',
        'Like and retweet',
        'Search tweets (basic keyword)',
    ],
    'Non-functional': [
        '300M DAU, 500M tweets/day => 5800 writes/sec',
        '100:1 read:write => 580K feed reads/sec',
        'Feed load < 2 seconds (p95)',
        '99.99% availability',
        'Tweets retained indefinitely (tweets never deleted by default)',
    ],
    'Estimates': [
        'Storage: 500M tweets * 300B = 150 GB/day, 54 TB/year',
        'Media: separate object store (S3), CDN-served',
        'Feed cache: 300M users * top-100-tweets * 100B = 3 TB (hot feeds in Redis)',
    ],
}
for category, items in reqs.items():
    print(f'{category}:')
    for item in items: print(f'  - {item}')
    print()

Distribusi Saat Penulisan: Umpan yang Telah Dihitung Sebelumnya

Dalam distribusi saat penulisan, ketika pengguna A menerbitkan kiriman, sistem segera mendistribusikannya ke umpan setiap pengikut. Ketika pengikut meminta umpannya, umpan tersebut sudah dihitung sebelumnya dan disimpan di Redis—pembacaan sederhana dari daftar Redis dengan O(k), dengan k sebagai ukuran umpan (biasanya dibatasi hingga 1000 kiriman).

Tantangannya adalah selebritas dengan jutaan pengikut yang menghasilkan operasi distribusi dalam jumlah sangat besar. Ketika Justin Bieber menerbitkan kiriman, sistem harus menulisnya ke lebih dari 100 juta umpan pengikut secara bersamaan—masalah nyata yang pernah dihadapi Twitter dan disebut sebagai “masalah selebritas”. Layanan distribusi saat penulisan harus berjalan secara asinkron dan berbasis antrean untuk menangani lonjakan ini.

# Fan-out on write (push model)
fan_out_steps = [
    '1. User posts tweet => write to tweets table (source of truth)',
    '2. Publish event to message queue (Kafka topic: tweet-created)',
    '3. Fan-out workers consume from queue:',
    '   a. Fetch list of followers from follows table',
    '   b. For each follower: LPUSH feed:{follower_id} tweet_id',
    '   c. Trim feed to last 1000 tweets: LTRIM feed:{follower_id} 0 999',
    '4. Feed read: LRANGE feed:{user_id} 0 99 => hydrate tweet_ids => response',
]
for step in fan_out_steps:
    print(step)

print('\nPros:')
print('  - Feed reads are O(1): just read from Redis list')
print('  - Feed is always sorted by recency automatically')
print('\nCons:')
print('  - Celebrities with 100M followers => 100M Redis writes per tweet')
print('  - Fan-out lag: followers may see tweet 10-30 seconds late at peak')
print('  - Inactive users waste Redis storage for precomputed feeds')

Distribusi Hibrida: Mengatasi Masalah Selebritas

Pendekatan hibrida menggabungkan distribusi saat penulisan untuk pengguna biasa dan distribusi saat pembacaan untuk selebritas. Seorang pengguna diklasifikasikan sebagai selebritas jika jumlah pengikutnya melebihi ambang batas (misalnya, 1 juta pengikut). Untuk pengguna biasa, kiriman didorong ke semua umpan pengikut saat diterbitkan. Untuk selebritas, kiriman mereka NOT didorong; sebaliknya, ketika seorang pengikut membaca umpannya, sistem mengambil kiriman terbaru selebritas tersebut dan menggabungkannya dengan umpan yang telah dihitung sebelumnya.

Model hibrida ini mendekati cara Twitter yang sebenarnya. Langkah penggabungan berlangsung cepat karena selebritas jarang menerbitkan kiriman dan penggabungan tersebut memiliki kompleksitas O(f), dengan f sebagai jumlah akun selebritas yang diikuti pengguna (biasanya sedikit).

# Hybrid fan-out implementation sketch
CELEBRITY_THRESHOLD = 1_000_000   # followers > 1M => celebrity

def on_post_tweet(user_id, tweet_id, follower_count):
    if follower_count <= CELEBRITY_THRESHOLD:
        # Fan-out to all followers (async via Kafka)
        print(f'User {user_id}: fan-out tweet {tweet_id} to {follower_count} followers')
        # => queue to fan-out workers
    else:
        print(f'Celebrity {user_id}: tweet {tweet_id} stored in timeline only')
        # => only write to tweets table + user timeline
        # => followers get it on demand when reading feed

def get_home_feed(user_id, followees):
    # 1. Get precomputed feed (fan-out on write tweets)
    precomputed = f'LRANGE feed:{user_id} 0 499'   # up to 500 tweets

    # 2. Find celebrity followees
    celebrity_followees = [u for u in followees if is_celebrity(u)]

    # 3. Fetch recent tweets from celebrities (fan-out on read)
    celebrity_tweets = []
    for celeb in celebrity_followees:
        tweets = f'GET tweets WHERE user_id={celeb} ORDER BY created_at DESC LIMIT 20'
        celebrity_tweets.extend(tweets)

    # 4. Merge and sort by recency
    combined = merge_and_sort(precomputed, celebrity_tweets)
    return combined[:100]

print('on_post_tweet for regular user:')
on_post_tweet('user123', 'tweet_abc', 500)
print('on_post_tweet for celebrity:')
on_post_tweet('celebrity456', 'tweet_xyz', 50_000_000)

Umpan Twitter: Arsitektur Lengkap

Arsitektur umpan Twitter yang lengkap menggabungkan beberapa sistem:

  • Layanan Kiriman: menulis kiriman ke Cassandra (kapasitas tulis tinggi, deret waktu)
  • Layanan Distribusi: pekerja asinkron (konsumen Kafka) yang mendorong ID kiriman ke umpan pengikut di Redis
  • Layanan Umpan: membaca umpan dari Redis, melengkapi ID kiriman menjadi objek kiriman lengkap, dan menggabungkan kiriman selebritas
  • Layanan Pengikutan: mengelola graf sosial (siapa mengikuti siapa) dalam basis data graf atau SQL yang dipartisi
  • Layanan Linimasa: menyajikan kiriman milik pengguna sendiri (terpisah dari umpan beranda)
# Twitter architecture summary
architecture = '''
[User] --> [API Gateway + Load Balancer]
                |
    +-----------+-----------+
    |           |           |
[Tweet Svc] [Feed Svc]  [Follow Svc]
    |           |           |
[Cassandra] [Redis Feeds] [Graph DB]
    |           |
[Kafka] <-- [Fan-out
    |        Workers]
[S3 + CDN]  (tweet_ids
(media)      => follower
             feed lists)

Key design choices:
- Tweets stored in Cassandra (PRIMARY KEY (user_id, created_at))
- Feed stored in Redis as list of tweet_ids per user (LPUSH/LTRIM/LRANGE)
- Fan-out via Kafka + workers (decoupled, retryable, scalable)
- Hybrid: regular users = push; celebrities = pull-on-read
- Hydration: tweet_ids -> full tweet objects via Cassandra read
'''
print(architecture)

Tajuk Pembatas Laju dan Respons Galat

Pembatas laju yang dirancang dengan baik mengomunikasikan batasnya kepada klien melalui tajuk respons HTTP. Hal ini memungkinkan klien menerapkan logika untuk mencoba lagi dan dasbor menampilkan penggunaan. Tajuk standar:

  • X-RateLimit-Limit: jumlah maksimum permintaan yang diizinkan dalam jendela
  • X-RateLimit-Remaining: jumlah permintaan yang tersisa dalam jendela saat ini
  • X-RateLimit-Reset: stempel waktu Unix saat jendela diatur ulang
  • Retry-After: jumlah detik yang harus ditunggu sebelum mencoba lagi (pada respons 429)

Kode status HTTP untuk respons yang terkena pembatasan laju adalah 429 Terlalu Banyak Permintaan.

# Rate limit response headers
def build_rate_limit_headers(limit, remaining, reset_timestamp, retry_after=None):
    headers = {
        'X-RateLimit-Limit': str(limit),
        'X-RateLimit-Remaining': str(max(0, remaining)),
        'X-RateLimit-Reset': str(int(reset_timestamp)),
    }
    if retry_after is not None:
        headers['Retry-After'] = str(retry_after)
    return headers

import time

# Simulated response for allowed request
headers = build_rate_limit_headers(
    limit=100,
    remaining=73,
    reset_timestamp=time.time() + 45
)
print('Allowed request headers:')
for k, v in headers.items():
    print(f'  {k}: {v}')

# Rate limited response
headers_429 = build_rate_limit_headers(
    limit=100,
    remaining=0,
    reset_timestamp=time.time() + 30,
    retry_after=30
)
print('\n429 Too Many Requests headers:')
for k, v in headers_429.items():
    print(f'  {k}: {v}')

Membandingkan Algoritme Pembatasan Laju

Perbandingan singkat semua algoritme pembatasan laju untuk membantu Anda memilih saat wawancara:

  • Ember Token: memungkinkan lonjakan permintaan dan memiliki laju pengisian ulang yang lancar. Paling cocok untuk antarmuka pemrograman aplikasi yang sesekali menerima lonjakan (pilihan yang paling umum).
  • Ember Bocor: memproses permintaan pada laju keluaran tetap, terlepas dari adanya lonjakan. Paling cocok untuk membentuk lalu lintas menjadi aliran konstan.
  • Penghitung Jendela Tetap: paling sederhana, dengan ruang O(1). Masalahnya adalah lonjakan sebesar dua kali batas pada batas jendela (misalnya, 100 pada 11:59 + 100 pada 12:00).
  • Log Jendela Geser: paling akurat, tanpa lonjakan pada batas. Masalahnya adalah memori O(jumlah permintaan).
  • Penghitung Jendela Geser: mendekati log jendela geser dengan ruang O(1). Digunakan oleh Cloudflare.
# Algorithm comparison matrix
comparison = [
    ('Token Bucket',          'Allows bursts',     'O(1)',         'Most APIs, default choice'),
    ('Leaky Bucket',          'Smooth output rate', 'O(1)',         'Traffic shaping, message queues'),
    ('Fixed Window Counter',  'Very simple',        'O(1)',         'Low-traffic, approximate OK'),
    ('Sliding Window Log',    'Most accurate',      'O(requests)', 'High-accuracy, low traffic'),
    ('Sliding Window Counter','Approximate+fast',   'O(1)',         'High-traffic, Cloudflare-style'),
]
print(f'{'Algorithm':30s} {'Burst Handling':20s} {'Memory':15s} {'Use Case'}')
print('-'*85)
for name, burst, mem, use in comparison:
    print(f'{name:30s} {burst:20s} {mem:15s} {use}')

Uji Cepat

Uji pemahaman Anda tentang konsep Struktur Data & Algoritme — Persiapan Wawancara Pemrograman dari pelajaran ini.

Rangkuman Pelajaran

Dalam pelajaran ini Anda mempelajari: pembatas laju menggunakan ember token (memungkinkan lonjakan), penghitung jendela geser (memori O(1)), atau log jendela geser (paling akurat) untuk mengendalikan laju permintaan, dan operasi atomik Redis memungkinkan pembatasan laju terdistribusi, umpan Twitter menggunakan penyebaran saat penulisan untuk melakukan pra-perhitungan umpan pengikut di Redis agar pembacaan cepat, dengan model penarikan hibrida untuk akun selebritas guna menghindari penggandaan penulisan yang sangat besar. Berikutnya, kita memasuki bagian proyek akhir dengan lembar contekan pengenalan pola yang memetakan sinyal soal ke pola algoritme yang menyelesaikannya dengan paling cepat.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Merancang Pembatas Laju dan Merancang Feed Twitter” gratis?

Ya — teks lengkap “Merancang Pembatas Laju dan Merancang Feed Twitter” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus DSA Interview Prep, upgrade ke CoddyKit PRO. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Merancang Pembatas Laju dan Merancang Feed Twitter”?

Terapkan kerangka tersebut pada dua masalah desain kanonis: pembatasan laju token-bucket/sliding-window dan feed berita fan-out-on-write vs fan-out-on-read Kamu berlatih DSA Interview Prep dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai DSA Interview Prep?

Tidak diperlukan pengalaman sebelumnya. DSA Interview Prep di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 4 dari 4.

Berapa lama pelajaran “Merancang Pembatas Laju dan Merancang Feed Twitter” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran DSA Interview Prep ini?

Ya. Setiap pelajaran DSA Interview Prep menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Kerangka Wawancara Desain Sistem
  2. Penyimpanan Data Skalabel: SQL vs NoSQL
  3. Caching, CDN, dan Penyeimbangan Beban
  4. Merancang Pembatas Laju dan Merancang Feed Twitter
← Kembali ke DSA Interview Prep