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