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

322 lines
8.9 KiB
Go

package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
// ============================================================================
// 💡 MASTER 16: MUSTERLÖSUNG (Microservice API)
// ============================================================================
// URLRecord speichert Metadaten zu einer gekürzten URL.
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"`
}
// ClickEvent repräsentiert ein asynchron verarbeitetes Klick-Ereignis.
type ClickEvent struct {
ShortCode string `json:"short_code"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Timestamp time.Time `json:"timestamp"`
}
// AnalyticsStats liefert aggregierte Zugriffs-Auswertungen.
type AnalyticsStats struct {
ShortCode string `json:"short_code"`
TotalClicks int64 `json:"total_clicks"`
UniqueIPs int `json:"unique_ips"`
}
// SystemMetrics liefert den globalen Zustand des Microservices.
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 hochgradig nebenläufige In-Memory Microservice.
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 den Server und Pufferkanäle.
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,
}
}
// StartWorkerPool startet Hintergrund-Worker zur Event-Verarbeitung.
func (s *MicroserviceServer) StartWorkerPool(numWorkers int) {
s.workerCount = numWorkers
for i := 0; i < numWorkers; i++ {
s.workerWG.Add(1)
go func() {
defer s.workerWG.Done()
for event := range s.clickQueue {
// 1. In Analytics-Historie speichern
s.analyticsMu.Lock()
s.analyticsStore[event.ShortCode] = append(s.analyticsStore[event.ShortCode], event)
s.analyticsMu.Unlock()
// 2. Globalen Verarbeitungszähler erhöhen
atomic.AddInt64(&s.totalProcessed, 1)
// 3. Klickzähler des URLs atomar hochzählen
s.storeMu.RLock()
if record, exists := s.urlStore[event.ShortCode]; exists {
atomic.AddInt64(&record.ClickCount, 1)
}
s.storeMu.RUnlock()
}
}()
}
}
// StopWorkerPool schließt die Warteschlange und wartet auf alle Worker.
func (s *MicroserviceServer) StopWorkerPool() {
close(s.clickQueue)
s.workerWG.Wait()
}
// ShortenURL validiert und kürzt eine URL thread-safe.
func (s *MicroserviceServer) ShortenURL(rawURL string) (*URLRecord, error) {
trimmed := strings.TrimSpace(rawURL)
if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") {
return nil, errors.New("invalid url format: must start with http:// or https://")
}
hash := sha256.Sum256([]byte(trimmed))
shortCode := hex.EncodeToString(hash[:])[:6]
s.storeMu.Lock()
defer s.storeMu.Unlock()
// Prüfen, ob bereits vorhanden
if existing, exists := s.urlStore[shortCode]; exists {
return existing, nil
}
record := &URLRecord{
ID: s.nextID,
ShortCode: shortCode,
OriginalURL: trimmed,
CreatedAt: time.Now(),
ClickCount: 0,
}
s.nextID++
s.urlStore[shortCode] = record
return record, nil
}
// ResolveURL findet die Original-URL und schickt ein Klick-Event an den Workerpool.
func (s *MicroserviceServer) ResolveURL(shortCode, ip, userAgent string) (string, error) {
s.storeMu.RLock()
record, exists := s.urlStore[shortCode]
s.storeMu.RUnlock()
if !exists {
return "", errors.New("url not found")
}
event := ClickEvent{
ShortCode: shortCode,
IP: ip,
UserAgent: userAgent,
Timestamp: time.Now(),
}
// Nicht-blockierender Dispatch in den Event-Bus
select {
case s.clickQueue <- event:
default:
// Queue voll: Event wird geloggt/ignoriert, um Request nicht zu verlangsamen
}
return record.OriginalURL, nil
}
// GetStats berechnet Klickzahlen und Unique-IPs.
func (s *MicroserviceServer) GetStats(shortCode string) (*AnalyticsStats, error) {
s.storeMu.RLock()
_, exists := s.urlStore[shortCode]
s.storeMu.RUnlock()
if !exists {
return nil, errors.New("url not found")
}
s.analyticsMu.RLock()
events := s.analyticsStore[shortCode]
s.analyticsMu.RUnlock()
uniqueIPs := make(map[string]bool)
for _, ev := range events {
uniqueIPs[ev.IP] = true
}
return &AnalyticsStats{
ShortCode: shortCode,
TotalClicks: int64(len(events)),
UniqueIPs: len(uniqueIPs),
}, nil
}
// GetSystemMetrics liefert Gesamtmetriken des Microservices.
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),
}
}
// SetupMicroserviceRouter konfiguriert alle HTTP-Routen.
func SetupMicroserviceRouter(srv *MicroserviceServer) *http.ServeMux {
mux := http.NewServeMux()
// 1. POST /api/shorten
mux.HandleFunc("/api/shorten", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var payload struct {
URL string `json:"url"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid json"})
return
}
record, err := srv.ShortenURL(payload.URL)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(record)
})
// 2. GET /r/{short_code}
mux.HandleFunc("/r/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
code := strings.TrimPrefix(r.URL.Path, "/r/")
code = strings.Trim(code, "/")
origURL, err := srv.ResolveURL(code, r.RemoteAddr, r.UserAgent())
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "short url not found"})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"original_url": origURL})
})
// 3. GET /api/stats/{short_code}
mux.HandleFunc("/api/stats/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
code := strings.TrimPrefix(r.URL.Path, "/api/stats/")
code = strings.Trim(code, "/")
stats, err := srv.GetStats(code)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "stats not found"})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(stats)
})
// 4. GET /api/metrics
mux.HandleFunc("/api/metrics", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
metrics := srv.GetSystemMetrics()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(metrics)
})
return mux
}
func main() {
fmt.Println("=== Master 16: Musterlösung ===")
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)
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)
stats, _ := srv.GetStats(rec.ShortCode)
fmt.Printf("Statistik: Clicks=%d, UniqueIPs=%d\n", stats.TotalClicks, stats.UniqueIPs)
srv.StopWorkerPool()
}