Hasura ile Gerçek Zamanlı Uygulama: Subscription Kullanımı

Gerçek zamanlı uygulamalar geliştirirken en çok zaman kaybettiren şey genellikle WebSocket altyapısını sıfırdan kurmak oluyor. Poll mekanizması yazmak, bağlantı yönetimini halletmek, reconnect mantığını kodlamak… Hasura bu işi dramatik biçimde kolaylaştırıyor ve subscription özelliği sayesinde veritabanı değişikliklerini anında istemcilere iletmek mümkün hale geliyor. Ben bu yazıda Hasura subscriptionlarını production ortamında nasıl kullandığımı, hangi tuzaklarla karşılaştığımı ve performansı nasıl optimize ettiğimi paylaşacağım.

Subscription Nedir ve Hasura Bunu Nasıl Hallediyor?

GraphQL subscriptionları, istemcinin sunucuyla kalıcı bir bağlantı kurmasını ve belirli olaylar gerçekleştiğinde otomatik olarak veri almasını sağlar. Klasik REST dünyasında bunu yapmak için ya long polling kullanırsınız ya da kendi WebSocket sunucunuzu yazarsınız. Her iki yaklaşım da bakım yükü getirir.

Hasura bu noktada farklı bir yol izliyor. PostgreSQL’in LISTEN/NOTIFY mekanizmasını ve kendi içindeki live query motorunu kullanarak subscription sorgularını yönetiyor. Yani siz sadece GraphQL subscription yazıyorsunuz, Hasura arkada PostgreSQL ile konuşup değişiklikleri yakalıyor ve bağlı tüm istemcilere gönderiyor.

Teknik olarak Hasura’nın subscription motoru şöyle çalışıyor:

  • İstemci bir subscription başlatır ve WebSocket üzerinden bağlantı açar
  • Hasura bu sorguyu “live query” olarak kaydeder
  • Belirlenen polling aralığında (varsayılan 1 saniye) PostgreSQL’e sorgu atar
  • Önceki sonuçla karşılaştırır, fark varsa istemciye push eder
  • Hasura 2.x ile gelen “streaming subscriptions” ise gerçek anlamda event-driven çalışır

Bu ayrım önemli çünkü “multiplexed live queries” ile birden fazla istemcinin aynı sorguyu çalıştırması durumunda Hasura sorguyu optimize edip tek seferde çalıştırır.

Ortamı Hazırlamak

Öncelikle çalışan bir Hasura instance’ına ihtiyacınız var. Docker ile hızlıca ayağa kaldırabilirsiniz:

version: '3.6'
services:
  postgres:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_PASSWORD: mysecretpassword
    volumes:
      - db_data:/var/lib/postgresql/data

  hasura:
    image: hasuraio/graphql-engine:v2.35.0
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:mysecretpassword@postgres:5432/postgres
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey

volumes:
  db_data:
docker-compose up -d
docker-compose logs -f hasura

Console açıldıktan sonra http://localhost:8080/console adresine gidin ve bir tablo oluşturun. Ben genellikle gerçek zamanlı uygulamalarda kullanılan klasik senaryolardan birini örnek alıyorum: sipariş takip sistemi.

Örnek Şema: Sipariş Takip

CREATE TABLE orders (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  customer_id UUID NOT NULL,
  status VARCHAR(50) NOT NULL DEFAULT 'pending',
  total_amount DECIMAL(10,2),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE order_events (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  order_id UUID REFERENCES orders(id),
  event_type VARCHAR(100) NOT NULL,
  payload JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_order_events_order_id ON order_events(order_id);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_status ON orders(status);

Bu tabloları Hasura Console’dan track edin ya da Hasura CLI ile migration olarak yönetin. Migration yaklaşımını tercih ediyorum çünkü versiyon kontrolü altında tutulması çok daha sağlıklı.

İlk Subscription Sorgusu

Hasura Console’daki GraphiQL üzerinden ya da herhangi bir GraphQL istemcisiyle subscription yazabilirsiniz. Temel bir sipariş durumu subscription’ı şöyle görünür:

subscription OrderStatusSubscription($orderId: uuid!) {
  orders_by_pk(id: $orderId) {
    id
    status
    updated_at
    order_events(order_by: {created_at: desc}, limit: 5) {
      event_type
      payload
      created_at
    }
  }
}

Bunu test etmek için bir terminalde veri değiştirin:

curl -X POST http://localhost:8080/v1/graphql 
  -H "Content-Type: application/json" 
  -H "x-hasura-admin-secret: myadminsecretkey" 
  -d '{
    "query": "mutation UpdateOrderStatus($id: uuid!, $status: String!) { update_orders_by_pk(pk_columns: {id: $id}, _set: {status: $status, updated_at: "now()"}) { id status } }",
    "variables": {
      "id": "your-order-uuid-here",
      "status": "processing"
    }
  }'

Subscription açık olan tarafta hemen güncelleme geldiğini göreceksiniz. Bu kadar. WebSocket kodu yok, NOTIFY trigger’ı yok, poll mantığı yok.

React ile Gerçek Dünya Kullanımı

Frontend tarafında Apollo Client veya urql kullanabilirsiniz. Ben urql’i tercih ediyorum çünkü bundle size açısından daha hafif. Ama Apollo Client daha geniş ekosisteme sahip. Örneği Apollo Client ile göstereyim:

// apollo-client.ts
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
import { getMainDefinition } from '@apollo/client/utilities'

const httpLink = new HttpLink({
  uri: 'http://localhost:8080/v1/graphql',
  headers: {
    'x-hasura-role': 'user',
  }
})

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:8080/v1/graphql',
    connectionParams: {
      headers: {
        Authorization: `Bearer ${getAuthToken()}`,
      }
    },
    retryAttempts: 10,
    shouldRetry: () => true,
  })
)

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query)
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    )
  },
  wsLink,
  httpLink
)

export const apolloClient = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
})

Component tarafında subscription kullanımı:

// OrderTracker.tsx
import { useSubscription, gql } from '@apollo/client'

const ORDER_SUBSCRIPTION = gql`
  subscription TrackOrder($orderId: uuid!, $customerId: uuid!) {
    orders(where: {
      id: {_eq: $orderId},
      customer_id: {_eq: $customerId}
    }) {
      id
      status
      total_amount
      updated_at
      order_events(order_by: {created_at: desc}, limit: 10) {
        id
        event_type
        payload
        created_at
      }
    }
  }
`

function OrderTracker({ orderId, customerId }) {
  const { data, loading, error } = useSubscription(ORDER_SUBSCRIPTION, {
    variables: { orderId, customerId }
  })

  if (loading) return <div>Bağlantı kuruluyor...</div>
  if (error) return <div>Hata: {error.message}</div>

  const order = data?.orders[0]
  if (!order) return <div>Sipariş bulunamadı</div>

  return (
    <div>
      <h2>Sipariş #{order.id}</h2>
      <p>Durum: {order.status}</p>
      <p>Son güncelleme: {new Date(order.updated_at).toLocaleString('tr-TR')}</p>
      <ul>
        {order.order_events.map(event => (
          <li key={event.id}>
            {event.event_type} - {new Date(event.created_at).toLocaleString('tr-TR')}
          </li>
        ))}
      </ul>
    </div>
  )
}

Streaming Subscriptions: Daha Verimli Yaklaşım

Hasura 2.x ile gelen streaming subscriptions, live query’den farklı olarak cursor tabanlı çalışır. Yani sadece yeni gelen kayıtları alırsınız, tüm sorguyu tekrar çalıştırmazsınız. Bu özellikle büyük tablolar için ciddi performans farkı yaratıyor.

subscription StreamOrderEvents($lastEventTime: timestamptz!) {
  order_events_stream(
    batch_size: 10,
    cursor: {
      initial_value: {created_at: $lastEventTime},
      ordering: ASC
    }
  ) {
    id
    order_id
    event_type
    payload
    created_at
  }
}

Bu sorguyu çalıştırırken dikkat etmeniz gereken şey: batch_size parametresi. Çok büyük koyarsanız başlangıçta yığın yığın veri gelir, çok küçük koyarsanız sık sık network roundtrip yaşarsınız. Ben genellikle 10-50 arası bir değerle başlarım, yüke göre ayarlarım.

Authorization ve Row-Level Security

Production’da her kullanıcının sadece kendi verisine erişebilmesi gerekiyor. Hasura’nın permission sistemi bunu şöyle hallediyor:

Hasura Console’dan orders tablosu için bir permission kuralı oluşturun:

# Hasura Console -> Data -> orders -> Permissions -> user role -> select
# Row filter:
{
  "customer_id": {
    "_eq": "X-Hasura-User-Id"
  }
}

Bu kural aktifken user rolüyle yapılan her subscription otomatik olarak sadece o kullanıcının siparişlerini döndürür. JWT içindeki x-hasura-user-id claim’ini kullanır. Yani frontend’de müşteri ID’sini manuel filtrelemenize gerek kalmaz, Hasura halleder.

JWT yapılandırması için environment variable:

HASURA_GRAPHQL_JWT_SECRET='{"type":"RS256","jwk_url":"https://your-auth-provider.com/.well-known/jwks.json"}'

Performans Optimizasyonu ve Tuzaklar

Production’a çıkınca öğrendiğim bazı şeyleri paylaşmadan geçemeyeceğim.

Polling interval ayarı: Varsayılan 1 saniyedir. Her tabloda bu kadar sıklığa ihtiyaç olmayabilir. Environment variable ile global olarak değiştirebilirsiniz:

HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=5000
# veya streaming için:
HASURA_GRAPHQL_STREAMING_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=1000

Connection limit problemi: Çok sayıda subscription bağlantısı PostgreSQL connection limitinizi zorlayabilir. Hasura bağlantı havuzu kullanıyor ama yine de dikkatli olun. PostgreSQL’in max_connections parametresini gözlemleyin:

# PostgreSQL'de aktif bağlantıları izlemek için
psql -U postgres -c "SELECT count(*), state FROM pg_stat_activity GROUP BY state;"

# Hasura'nın pool ayarları
HASURA_GRAPHQL_MAX_CONNECTIONS=50
HASURA_GRAPHQL_IDLE_TIMEOUT=180
HASURA_GRAPHQL_CONNECTION_LIFETIME=600

N+1 problemi subscriptionlarda: Subscription içinde ilişkili tablo sorguluyorsanız ve permission’lar karmaşıksa N+1 sorunu çıkabilir. Bunu erken tespit etmek için Hasura’nın query log özelliğini açın:

HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log,query-log

Logları izleyip yavaş sorguları yakalayın. Sonra explain analyze ile analiz edin:

EXPLAIN ANALYZE
SELECT o.*, oe.*
FROM orders o
LEFT JOIN order_events oe ON oe.order_id = o.id
WHERE o.customer_id = 'some-uuid'
ORDER BY oe.created_at DESC;

Subscription sayısını sınırlamak: Kötü niyetli ya da hatalı bir istemci binlerce subscription açabilir. Hasura’da bunu sınırlamak için:

HASURA_GRAPHQL_CONNECTION_COMPRESSION=true
# Rate limiting için Hasura Pro gerekiyor, 
# alternatif olarak önüne nginx koyun

Nginx ile WebSocket Proxy

Production’da Hasura önüne Nginx koyuyorsanız WebSocket için özel ayar gerekiyor:

# /etc/nginx/sites-available/hasura.conf
upstream hasura_backend {
    server localhost:8080;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.sirketiniz.com;

    ssl_certificate /etc/letsencrypt/live/api.sirketiniz.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.sirketiniz.com/privkey.pem;

    location /v1/graphql {
        proxy_pass http://hasura_backend;
        proxy_http_version 1.1;
        
        # WebSocket için kritik headerlar
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # Timeout ayarları - subscription bağlantıları uzun sürer
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_connect_timeout 60s;
        
        # Buffer'ları kapat - gerçek zamanlı için önemli
        proxy_buffering off;
        proxy_cache off;
    }
}

proxy_read_timeout değerini yüksek tutmazsanız Nginx bağlantıyı koparır ve istemci sürekli reconnect etmeye çalışır. Bunu ilk deployment’ta atlayıp sonradan debug etmek zorunda kaldım, gereksiz yere vakit kaybettim.

Monitoring ve Alerting

Subscription bağlantılarını izlemek için Hasura’nın metrics endpoint’ini kullanabilirsiniz:

# Prometheus metrics aktif etmek için
HASURA_GRAPHQL_ENABLED_APIS=metadata,graphql,config,metrics
HASURA_GRAPHQL_METRICS_SECRET=your-metrics-secret

# Metrics endpoint'i test etmek
curl -H "Authorization: Bearer your-metrics-secret" 
  http://localhost:8080/v1/metrics

Özellikle şu metriklere dikkat edin:

  • hasura_websocket_connections: Anlık WebSocket bağlantı sayısı
  • hasura_active_subscriptions: Aktif subscription sayısı
  • hasura_subscription_total_time_seconds: Subscription işlem süreleri
  • hasura_postgres_connections: PostgreSQL bağlantı havuzu durumu

Grafana’da bu metrikleri görselleştirmek için temel bir PromQL sorgusu:

# Aktif subscription sayısı
hasura_active_subscriptions

# WebSocket bağlantı sayısı trendi
rate(hasura_websocket_connections[5m])

# Yavaş subscription sorguları (1 saniyeden uzun)
histogram_quantile(0.95, hasura_subscription_total_time_seconds_bucket)

Alert kuralı olarak şunu öneririm: aktif subscription sayısı beklenmedik biçimde düşerse (sıfıra yaklaşırsa) alarm verin. Bu genellikle bir deployment sonrası WebSocket bağlantılarının düzgün taşınmadığını gösterir.

Gerçek Senaryo: Canlı Dashboard

Bir e-ticaret panelinde tüm aktif siparişleri canlı olarak izleyen bir dashboard yazdım. Başlangıçta her sipariş için ayrı subscription açıyordum, 50 sipariş için 50 subscription. Bu hem gereksiz hem de savurgan.

Doğru yaklaşım tek bir subscription ile tüm aktif siparişleri almak ve filtrelemeyi orada yapmak:

subscription ActiveOrdersDashboard {
  orders(
    where: {
      status: {_in: ["pending", "processing", "shipped"]}
    },
    order_by: {updated_at: desc},
    limit: 100
  ) {
    id
    status
    total_amount
    updated_at
    customer_id
  }
}

Bu subscription, ilgili siparişlerden herhangi birinde değişiklik olduğunda tüm listeyi günceller. Multiplexing sayesinde Hasura aynı sorguyu çalıştıran tüm admin kullanıcıları için tek bir PostgreSQL sorgusu çalıştırır. 20 admin aynı dashboard’u açsa bile PostgreSQL tarafında tek sorgu.

Sonuç

Hasura subscriptionları, WebSocket altyapısını sıfırdan inşa etme derdini tamamen ortadan kaldırıyor. Özellikle streaming subscriptions’ın gelmesiyle birlikte performans da ciddi ölçüde iyileşti. Ancak her sihirli araç gibi Hasura subscriptionlarının da sınırları var: çok karmaşık real-time mantık için event-driven mimariye (örneğin Kafka + Hasura event triggers kombinasyonu) geçmek gerekebilir.

Production’a taşımadan önce şu kontrol listesini uygulayın: PostgreSQL connection limitlerini gözden geçirin, Nginx’teki timeout ayarlarını yapın, permission kurallarınızın subscription sorgularını doğru biçimde kısıtladığını test edin ve polling interval’ı uygulamanızın ihtiyacına göre ayarlayın. Metrics endpoint’ini mutlaka Prometheus’a bağlayın, kör uçmayın.

Gerçek zamanlı özellikler kullanıcı deneyimini dramatik biçimde iyileştiriyor. Sipariş durumu güncellemeleri, canlı bildirimler, dashboard metrikleri gibi senaryolarda Hasura subscription’larını doğru yapılandırdığınızda hem geliştirme hızı hem de sistem kararlılığı açısından çok iyi sonuçlar alıyorsunuz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir