first commit

This commit is contained in:
Matthew-js 2025-10-03 14:32:45 +07:00
commit 40c37b5880
9 changed files with 444 additions and 0 deletions

51
config/db.go Normal file
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}