commit 40c37b58806199fe17bcec83bf8fdcd47ba9ce68 Author: Matthew-js Date: Fri Oct 3 14:32:45 2025 +0700 first commit diff --git a/config/db.go b/config/db.go new file mode 100644 index 0000000..8b8b819 --- /dev/null +++ b/config/db.go @@ -0,0 +1,51 @@ +package config + +import ( + "database/sql" + "fmt" + "time" + + _ "github.com/lib/pq" +) + +// DB global var (may be nil if connection fails) +var DB *sql.DB + +// Use the credentials you provided +const ( + host = "202.46.28.160" + port = 45432 + user = "5803024022" + password = "PW5803024022" + dbname = "tgs03_5803024022" +) + +// InitDB attempts to open a connection pool to Postgres. +// It returns error if ping fails. +func InitDB() (*sql.DB, error) { + dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err + } + // reasonable settings + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(time.Minute * 5) + + // Ping with timeout + done := make(chan error, 1) + go func() { + done <- db.Ping() + }() + select { + case err := <-done: + if err != nil { + return nil, err + } + case <-time.After(2 * time.Second): + return nil, fmt.Errorf("db ping timeout") + } + return db, nil +} diff --git a/context_handler/context_handler.go b/context_handler/context_handler.go new file mode 100644 index 0000000..3b537aa --- /dev/null +++ b/context_handler/context_handler.go @@ -0,0 +1,43 @@ +package context_handler + +import ( + "context" + "errors" + "fmt" + "time" +) + +type keyType string + +const userKey keyType = "userID" + +// WithUserID stores user id in context +func WithUserID(ctx context.Context, userID string) context.Context { + return context.WithValue(ctx, userKey, userID) +} + +// FetchRiwayatKursus simulates fetching course history but respects context deadlines/cancel. +func FetchRiwayatKursus(ctx context.Context) (string, error) { + // try to read user ID + uid, _ := ctx.Value(userKey).(string) + if uid == "" { + uid = "unknown" + } + + // simulate work that may take up to 2 seconds + work := make(chan string, 1) + + go func() { + // simulate slower work 1.5s + time.Sleep(1500 * time.Millisecond) + work <- fmt.Sprintf("history for user %s: [Course-A, Course-B]", uid) + }() + + select { + case <-ctx.Done(): + // context timed out or cancelled + return "", errors.New("fetch cancelled or timed out: " + ctx.Err().Error()) + case res := <-work: + return res, nil + } +} diff --git a/enrollment/enrollment.go b/enrollment/enrollment.go new file mode 100644 index 0000000..73d6084 --- /dev/null +++ b/enrollment/enrollment.go @@ -0,0 +1,79 @@ +package enrollment + +import ( + "fmt" + "sync" + "time" +) + +// IdempotencyStore stores results keyed by idempotency key. +var ( + store map[string]string + storeMutex sync.RWMutex +) + +// InitStore initializes the in-memory store. +func InitStore() { + store = make(map[string]string) +} + +// EnrollCourseIdempotent processes an enrollment with idempotency key. +// If key exists, returns existing result immediately. If not, simulates +// 1.5s work, stores and returns result. +func EnrollCourseIdempotent(key string) string { + // check read lock first + storeMutex.RLock() + if res, ok := store[key]; ok { + storeMutex.RUnlock() + return res + } + storeMutex.RUnlock() + + // Acquire write lock to ensure only one goroutine does the processing + storeMutex.Lock() + // double-check after acquiring write lock + if res, ok := store[key]; ok { + storeMutex.Unlock() + return res + } + + // Simulate processing (1.5s) + time.Sleep(1500 * time.Millisecond) + res := fmt.Sprintf("enrolled-successful-for-key-%s", key) + + // Save and release lock + store[key] = res + storeMutex.Unlock() + return res +} + +// RunConcurrentEnrollments runs n goroutines that call EnrollCourseIdempotent with same key. +// Returns map: which goroutine got what result and runtime info. +func RunConcurrentEnrollments(key string, n int) map[string]interface{} { + var wg sync.WaitGroup + wg.Add(n) + resCh := make(chan string, n) + start := time.Now() + + for i := 0; i < n; i++ { + go func(id int) { + defer wg.Done() + r := EnrollCourseIdempotent(key) + resCh <- fmt.Sprintf("goroutine-%d: %s", id+1, r) + }(i) + } + + wg.Wait() + close(resCh) + out := make([]string, 0, n) + for r := range resCh { + out = append(out, r) + } + duration := time.Since(start) + return map[string]interface{}{ + "key": key, + "count": n, + "duration": duration.String(), + "results": out, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2222d24 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module elearning-backend + +go 1.25.0 + +require github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aeddeae --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8420fe0 --- /dev/null +++ b/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + _ "github.com/lib/pq" + + "tgs03-backend/config" + "tgs03-backend/context_handler" + "tgs03-backend/enrollment" + "tgs03-backend/poll" + "tgs03-backend/pool" + "tgs03-backend/worker" +) + +func writeJSON(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(v) +} + +func main() { + // initialize DB (attempt). If DB not reachable, still continue (some features simulate). + db, err := config.InitDB() + if err != nil { + log.Printf("Warning: failed connect DB: %v (continuing in simulation mode)", err) + } else { + // keep db on global package + defer db.Close() + config.DB = db + } + + // Initialize pool manager (simulated connections) + pm := pool.NewDBManager(5) // capacity 5 + + // Initialize idempotency store + enrollment.InitStore() + + mux := http.NewServeMux() + + // Endpoint: /sertifikasi -> run 50 goroutines with WaitGroup (Tugas 1.1) + mux.HandleFunc("/sertifikasi", func(w http.ResponseWriter, r *http.Request) { + n := 50 + start := time.Now() + results := worker.RunSertifikasi(n) + duration := time.Since(start) + writeJSON(w, map[string]interface{}{ + "count": n, + "duration": duration.String(), + "results": results, + }) + }) + + // Endpoint: /pool -> run 20 goroutines that call EksekusiQuery (Tugas 1.2) + mux.HandleFunc("/pool", func(w http.ResponseWriter, r *http.Request) { + n := 20 + // run queries concurrently + res := pm.RunQueries(n) + writeJSON(w, map[string]interface{}{ + "requested": n, + "results": res, + }) + }) + + // Endpoint: /poll -> worker pool 5 workers, send 100 jobs (Tugas 1.3) + mux.HandleFunc("/poll", func(w http.ResponseWriter, r *http.Request) { + jobCount := 100 + workers := 5 + results := poll.RunPollSimulation(workers, jobCount) + writeJSON(w, map[string]interface{}{ + "workers": workers, + "job_count": jobCount, + "results": results, + }) + }) + + // Endpoint: /context -> call FetchRiwayatKursus with 1s timeout (Tugas 2.1 & 2.2) + mux.HandleFunc("/context", func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) + defer cancel() + // attach user id + ctx = context_handler.WithUserID(ctx, "REQ-456") + + start := time.Now() + res, err := context_handler.FetchRiwayatKursus(ctx) + duration := time.Since(start) + if err != nil { + writeJSON(w, map[string]interface{}{ + "error": err.Error(), + "duration": duration.String(), + }) + return + } + writeJSON(w, map[string]interface{}{ + "duration": duration.String(), + "data": res, + }) + }) + + // Endpoint: /enroll -> demonstrates idempotent enrollment (Tugas 2.3) + // Query params: key (idempotency key). If omitted uses "idem-key-sample". + mux.HandleFunc("/enroll", func(w http.ResponseWriter, r *http.Request) { + key := r.URL.Query().Get("key") + if key == "" { + key = "idem-key-sample" + } + // run 5 goroutines concurrently with same key to demonstrate idempotency + results := enrollment.RunConcurrentEnrollments(key, 5) + writeJSON(w, results) + }) + + addr := ":8080" + fmt.Printf("Server running at %s\n", addr) + log.Fatal(http.ListenAndServe(addr, mux)) +} diff --git a/poll/poll_worker.go b/poll/poll_worker.go new file mode 100644 index 0000000..557c11c --- /dev/null +++ b/poll/poll_worker.go @@ -0,0 +1,49 @@ +package poll + +import ( + "fmt" + "sync" + "time" +) + +// PollJobWorker is a worker that processes jobs from channel. +func PollJobWorker(id int, jobs <-chan int, results chan<- string, wg *sync.WaitGroup) { + defer wg.Done() + for job := range jobs { + // simulate processing time + time.Sleep(time.Millisecond * 20) + res := fmt.Sprintf("worker-%d processed job-%d", id, job) + results <- res + } +} + +// RunPollSimulation launches `workers` workers and sends `jobCount` jobs. +func RunPollSimulation(workers, jobCount int) []string { + jobs := make(chan int, 10) // capacity 10 as requested + results := make(chan string, jobCount) + var wg sync.WaitGroup + + // start workers + wg.Add(workers) + for w := 1; w <= workers; w++ { + go PollJobWorker(w, jobs, results, &wg) + } + + // send jobs + for j := 1; j <= jobCount; j++ { + jobs <- j + } + close(jobs) // close after sending all jobs + + // wait for workers to finish then close results + go func() { + wg.Wait() + close(results) + }() + + out := make([]string, 0, jobCount) + for r := range results { + out = append(out, r) + } + return out +} diff --git a/pool/pool_manager.go b/pool/pool_manager.go new file mode 100644 index 0000000..983b68b --- /dev/null +++ b/pool/pool_manager.go @@ -0,0 +1,64 @@ +package pool + +import ( + "fmt" + "sync" + "time" +) + +// DBManager simulates a connection pool using a channel of "connections". +type DBManager struct { + pool chan int +} + +// NewDBManager creates a pool manager with capacity `cap`. +func NewDBManager(cap int) *DBManager { + p := make(chan int, cap) + // initialize simulated connections IDs 1..cap + for i := 1; i <= cap; i++ { + p <- i + } + return &DBManager{pool: p} +} + +// getConn pulls a connection id from pool (blocking if none available) +func (m *DBManager) getConn() int { + return <-m.pool +} + +// releaseConn returns a connection id to the pool +func (m *DBManager) releaseConn(id int) { + m.pool <- id +} + +// EksekusiQuery simulates executing a query: take connection, wait 1s, release. +func (m *DBManager) EksekusiQuery(query string) string { + conn := m.getConn() + // Simulate query execution + time.Sleep(time.Second * 1) + m.releaseConn(conn) + return fmt.Sprintf("conn-%d executed: %s", conn, query) +} + +// RunQueries launches n goroutines that call EksekusiQuery. +// Returns list of responses in the order goroutines finished. +func (m *DBManager) RunQueries(n int) []string { + var wg sync.WaitGroup + wg.Add(n) + resCh := make(chan string, n) + for i := 0; i < n; i++ { + q := fmt.Sprintf("SELECT %d", i+1) + go func(query string) { + defer wg.Done() + res := m.EksekusiQuery(query) + resCh <- res + }(q) + } + wg.Wait() + close(resCh) + out := make([]string, 0, n) + for s := range resCh { + out = append(out, s) + } + return out +} diff --git a/worker/worker.go b/worker/worker.go new file mode 100644 index 0000000..51d1111 --- /dev/null +++ b/worker/worker.go @@ -0,0 +1,32 @@ +package worker + +import ( + "fmt" + "sync" + "time" +) + +// ProsesSertifikasi simulates certification processing for a single user. +func ProsesSertifikasi(userID int) string { + // simulate variable work (sleep) + time.Sleep(time.Millisecond * time.Duration(50+userID%10*20)) + return fmt.Sprintf("user-%d: done", userID) +} + +// RunSertifikasi starts n goroutines and waits for all to finish. +// Returns results in a slice. +func RunSertifikasi(n int) []string { + var wg sync.WaitGroup + wg.Add(n) + + results := make([]string, n) + for i := 0; i < n; i++ { + i := i // capture + go func() { + defer wg.Done() + results[i] = ProsesSertifikasi(i + 1) + }() + } + wg.Wait() + return results +}