Aufgaben-Python/courses/go/16_master_abschlussprojekt_microservice/aufgabe.go

187 lines
6.1 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
// ============================================================================
// 🏆 MASTER 16: HIGH-CONCURRENCY URL SHORTENER & ANALYTICS MICROSERVICE
// ============================================================================
// 🎯 TODO 1a: Definiere das Struct 'URLRecord'
type URLRecord struct {
ID int `json:"id"`
ShortCode string `json:"short_code"`
OriginalURL string `json:"original_url"`
CreatedAt time.Time `json:"created_at"`
ClickCount int64 `json:"click_count"`
}
// 🎯 TODO 1b: Definiere das Struct 'ClickEvent'
type ClickEvent struct {
ShortCode string `json:"short_code"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp"`
}
// 🎯 TODO 1c: Definiere das Struct 'AnalyticsStats'
type AnalyticsStats struct {
ShortCode string `json:"short_code"`
TotalClicks int64 `json:"total_clicks"`
UniqueIPs int `json:"unique_ips"`
}
// 🎯 TODO 1d: Definiere das Struct 'SystemMetrics'
type SystemMetrics struct {
TotalURLs int `json:"total_urls"`
TotalClicksProcessed int64 `json:"total_clicks_processed"`
ActiveWorkers int `json:"active_workers"`
QueueLength int `json:"queue_length"`
}
// MicroserviceServer ist der zentrale Zustand des Microservices.
type MicroserviceServer struct {
urlStore map[string]*URLRecord
analyticsStore map[string][]ClickEvent
storeMu sync.RWMutex
analyticsMu sync.RWMutex
clickQueue chan ClickEvent
workerWG sync.WaitGroup
totalProcessed int64
workerCount int
nextID int
}
// NewMicroserviceServer initialisiert die Strukturen und den Event-Kanal.
func NewMicroserviceServer(queueCapacity int) *MicroserviceServer {
if queueCapacity <= 0 {
queueCapacity = 1000
}
return &MicroserviceServer{
urlStore: make(map[string]*URLRecord),
analyticsStore: make(map[string][]ClickEvent),
clickQueue: make(chan ClickEvent, queueCapacity),
nextID: 1,
}
}
// 🎯 TODO 2a: Implementiere 'StartWorkerPool'
// Parameter:
// - numWorkers: Anzahl parallel laufender Worker-Goroutines
//
// Anforderungen:
// 1. Speichere s.workerCount = numWorkers.
// 2. Starte für jeden Worker (1..numWorkers) eine Goroutine mit s.workerWG.Add(1).
// 3. Jede Goroutine liest mit 'for event := range s.clickQueue':
// - Speichert das Event in s.analyticsStore[event.ShortCode] (mit s.analyticsMu.Lock()).
// - Erhöht s.totalProcessed atomar mit atomic.AddInt64(&s.totalProcessed, 1).
// - Erhöht atomar den ClickCount des zugehörigen URLRecords in s.urlStore.
// 4. Bei Schließen des Channels: defer s.workerWG.Done().
func (s *MicroserviceServer) StartWorkerPool(numWorkers int) {
// TODO: Worker-Goroutines starten und mit WaitGroup synchronisieren
}
// 🎯 TODO 2b: Implementiere 'StopWorkerPool'
// Schließt die clickQueue und wartet mit s.workerWG.Wait() auf die Beendigung aller Worker.
func (s *MicroserviceServer) StopWorkerPool() {
// TODO: close(s.clickQueue) und s.workerWG.Wait()
}
// 🎯 TODO 3: Implementiere 'ShortenURL'
// Parameter:
// - rawURL: Die lange Original-URL (z.B. "https://go.dev/doc")
// Rückgabe:
// - Zeiger auf URLRecord und nil bei Erfolg
// - nil und Fehler bei ungültiger URL
//
// Anforderungen:
// 1. Validiere rawURL: Muss mit "http://" oder "https://" beginnen (sonst errors.New("invalid url format")).
// 2. Erzeuge einen ShortCode (z.B. die ersten 6 Zeichen des SHA256-Hashes von rawURL).
// 3. Speichere den URLRecord thread-safe in s.urlStore.
func (s *MicroserviceServer) ShortenURL(rawURL string) (*URLRecord, error) {
// TODO: URL validieren, ShortCode generieren, in Store speichern
return nil, nil
}
// 🎯 TODO 4: Implementiere 'ResolveURL'
// Parameter:
// - shortCode: Der Kurzcode
// - ip: IP-Adresse des anfragenden Clients
// - userAgent: User-Agent des Clients
// Rückgabe:
// - Original-URL und nil bei Fund
// - "", errors.New("url not found") bei unbekanntem Code
//
// Anforderungen:
// 1. Hole den URLRecord thread-safe mit s.storeMu.RLock().
// 2. Erstelle ein ClickEvent mit Timestamp time.Now().
// 3. Sende das Event nicht-blockierend (select mit default) in s.clickQueue.
// 4. Gib record.OriginalURL zurück.
func (s *MicroserviceServer) ResolveURL(shortCode, ip, userAgent string) (string, error) {
// TODO: URL auflösen und Klick-Event asynchron dispatchen
return "", nil
}
// 🎯 TODO 5: Implementiere 'GetStats'
// Liefert Klick-Statistiken und die Anzahl der Unique IPs für einen ShortCode.
func (s *MicroserviceServer) GetStats(shortCode string) (*AnalyticsStats, error) {
// TODO: s.analyticsMu.RLock(), Events zählen und Unique IPs berechnen
return nil, nil
}
// GetSystemMetrics liefert Gesamt-Metriken.
func (s *MicroserviceServer) GetSystemMetrics() SystemMetrics {
s.storeMu.RLock()
totalURLs := len(s.urlStore)
s.storeMu.RUnlock()
return SystemMetrics{
TotalURLs: totalURLs,
TotalClicksProcessed: atomic.LoadInt64(&s.totalProcessed),
ActiveWorkers: s.workerCount,
QueueLength: len(s.clickQueue),
}
}
// 🎯 TODO 6: Implementiere 'SetupMicroserviceRouter'
// Registriert alle REST Endpunkte:
// - POST /api/shorten
// - GET /r/
// - GET /api/stats/
// - GET /api/metrics
func SetupMicroserviceRouter(srv *MicroserviceServer) *http.ServeMux {
mux := http.NewServeMux()
// TODO: HTTP-Handler registrieren
return mux
}
func main() {
fmt.Println("=== Master 16: URL Shortener & Analytics Microservice ===")
srv := NewMicroserviceServer(1000)
srv.StartWorkerPool(4)
rec, _ := srv.ShortenURL("https://golang.org")
fmt.Printf("Kurz-URL erstellt: Code='%s' -> '%s'\n", rec.ShortCode, rec.OriginalURL)
// Klick simulieren
url, _ := srv.ResolveURL(rec.ShortCode, "127.0.0.1", "Go-Client/1.0")
fmt.Printf("URL aufgelöst: %s\n", url)
time.Sleep(50 * time.Millisecond) // Worker kurz Zeit zur Verarbeitung geben
stats, _ := srv.GetStats(rec.ShortCode)
fmt.Printf("Statistik: Clicks=%d, UniqueIPs=%d\n", stats.TotalClicks, stats.UniqueIPs)
srv.StopWorkerPool()
}