first commit
This commit is contained in:
commit
40c37b5880
51
config/db.go
Normal file
51
config/db.go
Normal file
@ -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
|
||||||
|
}
|
43
context_handler/context_handler.go
Normal file
43
context_handler/context_handler.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
79
enrollment/enrollment.go
Normal file
79
enrollment/enrollment.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module elearning-backend
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/lib/pq v1.10.9
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -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=
|
119
main.go
Normal file
119
main.go
Normal file
@ -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))
|
||||||
|
}
|
49
poll/poll_worker.go
Normal file
49
poll/poll_worker.go
Normal file
@ -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
|
||||||
|
}
|
64
pool/pool_manager.go
Normal file
64
pool/pool_manager.go
Normal file
@ -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
|
||||||
|
}
|
32
worker/worker.go
Normal file
32
worker/worker.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user