first commit
This commit is contained in:
commit
4f291c93e5
72
client.go
Normal file
72
client.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
UseUnixSocket bool // true = unix socket, false = http
|
||||||
|
SocketPath string // e.g. /tmp/shorty.sock
|
||||||
|
HTTPAddress string // e.g. http://localhost:8080
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
baseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(cfg ClientConfig) (*Client, error) {
|
||||||
|
transport := &http.Transport{}
|
||||||
|
|
||||||
|
if cfg.UseUnixSocket {
|
||||||
|
dialer := func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return net.Dial("unix", cfg.SocketPath)
|
||||||
|
}
|
||||||
|
transport.DialContext = dialer
|
||||||
|
cfg.HTTPAddress = "http://unix" // dummy for request building
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: cfg.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
httpClient: client,
|
||||||
|
baseURL: cfg.HTTPAddress,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Shorten(url string) (string, error) {
|
||||||
|
body := []byte(url)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", c.baseURL+"/s", bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", errors.New("shorten failed: " + string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result), nil
|
||||||
|
}
|
19
examples/main.go
Normal file
19
examples/main.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.tijl.dev/tijl/shortify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
s, err := shortify.NewShortener(shortify.Config{
|
||||||
|
DataFolder: "./shortify",
|
||||||
|
CacheSize: 10000,
|
||||||
|
AccessLogSize: 10000,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.ServeSocket("./data/shortify.sock")
|
||||||
|
s.ServeHTTP("0.0.0.0:3001")
|
||||||
|
}
|
218
generation.go
Normal file
218
generation.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IDPool struct {
|
||||||
|
db *bbolt.DB
|
||||||
|
mu sync.Mutex
|
||||||
|
idLen int
|
||||||
|
poolCap int
|
||||||
|
regenThresh int
|
||||||
|
inMemoryPool []string
|
||||||
|
cond *sync.Cond
|
||||||
|
stopCh chan struct{}
|
||||||
|
usedChan chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDPool(db *bbolt.DB, idLen int, poolCap int, regenThresh int) (*IDPool, error) {
|
||||||
|
p := &IDPool{
|
||||||
|
db: db,
|
||||||
|
idLen: idLen,
|
||||||
|
poolCap: poolCap,
|
||||||
|
regenThresh: regenThresh,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
usedChan: make(chan string, 1000),
|
||||||
|
}
|
||||||
|
p.cond = sync.NewCond(&p.mu)
|
||||||
|
|
||||||
|
err := db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists(idpoolBucket)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.loadFromDB(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.inMemoryPool) == 0 {
|
||||||
|
// idpool empty at startup, generating initial batch...
|
||||||
|
if err := p.GenerateBatch(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := p.loadFromDB(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go p.backgroundGenerator()
|
||||||
|
go p.flushUsedIDs()
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IDPool) loadFromDB() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
|
||||||
|
err := p.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket(idpoolBucket)
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
||||||
|
ids = append(ids, string(k))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.inMemoryPool = ids
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IDPool) GenerateBatch() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
return p.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket(idpoolBucket)
|
||||||
|
count := b.Stats().KeyN
|
||||||
|
for count < p.poolCap {
|
||||||
|
id, err := generateID(p.idLen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b.Get([]byte(id)) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.Put([]byte(id), []byte{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PopID returns an ID from memory and queues it for async DB removal
|
||||||
|
func (p *IDPool) PopID() (string, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if len(p.inMemoryPool) == 0 {
|
||||||
|
return "", errors.New("id pool empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast O(1) pop
|
||||||
|
id := p.inMemoryPool[0]
|
||||||
|
p.inMemoryPool = p.inMemoryPool[1:]
|
||||||
|
|
||||||
|
// Queue for async delete
|
||||||
|
select {
|
||||||
|
case p.usedChan <- id:
|
||||||
|
default:
|
||||||
|
// If the channel is full, we drop the delete. Risky only if shutdown happens
|
||||||
|
log.Println("Warning: used ID queue full; delete may be delayed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal for batch regen if low
|
||||||
|
if len(p.inMemoryPool) < p.regenThresh {
|
||||||
|
p.cond.Signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IDPool) backgroundGenerator() {
|
||||||
|
for {
|
||||||
|
p.mu.Lock()
|
||||||
|
for len(p.inMemoryPool) >= p.regenThresh {
|
||||||
|
p.cond.Wait()
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// generating batch
|
||||||
|
err := p.GenerateBatch()
|
||||||
|
if err != nil {
|
||||||
|
// error generating batch:(
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := p.loadFromDB(); err != nil {
|
||||||
|
// error laoding from db:(
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IDPool) flushUsedIDs() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
return
|
||||||
|
case id := <-p.usedChan:
|
||||||
|
err := p.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket(idpoolBucket)
|
||||||
|
return b.Delete([]byte(id))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to delete used ID %s: %v\n", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IDPool) Stop() {
|
||||||
|
close(p.stopCh)
|
||||||
|
// Drain and flush remaining used IDs
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case id := <-p.usedChan:
|
||||||
|
p.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket(idpoolBucket)
|
||||||
|
return b.Delete([]byte(id))
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var idpoolBucket = []byte("idpool")
|
||||||
|
var base62 = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||||
|
|
||||||
|
func generateID(n int) (string, error) {
|
||||||
|
id := make([]rune, n)
|
||||||
|
for i := range id {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(base62))))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
id[i] = base62[num.Int64()]
|
||||||
|
}
|
||||||
|
return string(id), nil
|
||||||
|
}
|
35
global.go
Normal file
35
global.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global instance
|
||||||
|
var (
|
||||||
|
Global *Shortener
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(cfg Config) error {
|
||||||
|
var err error
|
||||||
|
once.Do(func() {
|
||||||
|
Global, err = NewShortener(cfg)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global instance (duc)
|
||||||
|
var (
|
||||||
|
GlobalIDPool *IDPool
|
||||||
|
oncePopper sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitIDPool(db *bbolt.DB, idLen, poolCap, regenThresh int) error {
|
||||||
|
var err error
|
||||||
|
oncePopper.Do(func() {
|
||||||
|
GlobalIDPool, err = NewIDPool(db, idLen, poolCap, regenThresh)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
72
logger.go
Normal file
72
logger.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/marcboeker/go-duckdb/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VisitLog struct {
|
||||||
|
ShortID string
|
||||||
|
LongURL string
|
||||||
|
|
||||||
|
IP string
|
||||||
|
|
||||||
|
UserAgent string
|
||||||
|
Referer string
|
||||||
|
Language string
|
||||||
|
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) LogVisit(log VisitLog) {
|
||||||
|
s.logChan <- log
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) startLogging() {
|
||||||
|
|
||||||
|
db, err := sql.Open("duckdb", s.DataFolder+"/analytics.db")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS seq_visitid START 1;
|
||||||
|
CREATE TABLE IF NOT EXISTS visits (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
shortid VARCHAR,
|
||||||
|
longurl VARCHAR,
|
||||||
|
ip VARCHAR,
|
||||||
|
useragent VARCHAR,
|
||||||
|
referer VARCHAR,
|
||||||
|
language VARCHAR,
|
||||||
|
time TIMESTAMP
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := db.Prepare(`
|
||||||
|
INSERT INTO visits (id, shortid, longurl, ip, useragent, referer, language, time)
|
||||||
|
VALUES (nextval('seq_visitid'), ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
for log := range s.logChan {
|
||||||
|
_, err = stmt.Exec(log.ShortID, log.LongURL, log.IP, log.UserAgent, log.Referer, log.Language, log.Time)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
server.go
Normal file
122
server.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wow
|
||||||
|
func (s *Shortener) ServeSocket(path string) {
|
||||||
|
|
||||||
|
// Remove old socket if exists
|
||||||
|
socketPath := path
|
||||||
|
_ = os.Remove(socketPath)
|
||||||
|
|
||||||
|
// Create a Unix socket listener
|
||||||
|
ln, err := net.Listen("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to listen on unix socket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally set permissions so other processes can connect
|
||||||
|
_ = os.Chmod(socketPath, 0666)
|
||||||
|
|
||||||
|
app := fiber.New()
|
||||||
|
|
||||||
|
app.Post("/s", s.HandlePOSTShortURLDirect())
|
||||||
|
|
||||||
|
log.Fatal(app.Listener(ln))
|
||||||
|
}
|
||||||
|
func (s *Shortener) ServeHTTP(addr string) {
|
||||||
|
app := fiber.New()
|
||||||
|
|
||||||
|
app.Get("/s/:id", s.HandleGETShortURL())
|
||||||
|
|
||||||
|
log.Fatal(app.Listen(addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) NewShortURL(longUrl string) string {
|
||||||
|
shortID, err := s.idPool.PopID()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.put(shortID, longUrl)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) HandleGETShortURL() func(*fiber.Ctx) error {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
shortID := c.Params("id")
|
||||||
|
|
||||||
|
url, err := s.get(shortID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.LogVisit(VisitLog{
|
||||||
|
ShortID: shortID,
|
||||||
|
LongURL: url,
|
||||||
|
|
||||||
|
IP: c.IP(),
|
||||||
|
|
||||||
|
UserAgent: string(c.Context().UserAgent()),
|
||||||
|
Language: c.GetReqHeaders()["Accept-Language"][0],
|
||||||
|
Referer: string(c.Request().Header.Referer()),
|
||||||
|
|
||||||
|
Time: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Set("Referrer-Policy", "no-referrer")
|
||||||
|
return c.Redirect(url, fiber.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) HandlePOSTShortURL() func(*fiber.Ctx) error {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
longUrl := string(c.Body())
|
||||||
|
|
||||||
|
if longUrl == "" {
|
||||||
|
return errors.New("whut")
|
||||||
|
}
|
||||||
|
|
||||||
|
shortID, err := s.idPool.PopID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.put(shortID, longUrl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendString(shortID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) HandlePOSTShortURLDirect() func(*fiber.Ctx) error {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
longUrl := string(c.Body())
|
||||||
|
shortID := c.Query("s")
|
||||||
|
|
||||||
|
if longUrl == "" || shortID == "" {
|
||||||
|
return errors.New("no comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.put(shortID, longUrl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendString(shortID)
|
||||||
|
}
|
||||||
|
}
|
58
shortify.go
Normal file
58
shortify.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/golang-lru"
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Shortener struct {
|
||||||
|
DataFolder string
|
||||||
|
|
||||||
|
db *bolt.DB
|
||||||
|
cache *lru.Cache
|
||||||
|
accessCache *lru.Cache
|
||||||
|
logChan chan VisitLog
|
||||||
|
idPool *IDPool
|
||||||
|
|
||||||
|
writeChan chan [2]string // queue of writes: [shortID, longURL]
|
||||||
|
memStore sync.Map // thread-safe built-in
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DataFolder string
|
||||||
|
CacheSize int
|
||||||
|
AccessLogSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShortener(cfg Config) (*Shortener, error) {
|
||||||
|
db, err := bolt.Open(cfg.DataFolder+"/database.db", 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
urlCache, _ := lru.New(cfg.CacheSize)
|
||||||
|
accessCache, _ := lru.New(cfg.AccessLogSize)
|
||||||
|
|
||||||
|
idPool, err := NewIDPool(db, 8, 10000, 2000)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Shortener{
|
||||||
|
DataFolder: cfg.DataFolder,
|
||||||
|
db: db,
|
||||||
|
cache: urlCache,
|
||||||
|
accessCache: accessCache,
|
||||||
|
logChan: make(chan VisitLog, 1000),
|
||||||
|
writeChan: make(chan [2]string, 1000),
|
||||||
|
idPool: idPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.startLogging()
|
||||||
|
go s.asyncDBWriter()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
71
storage.go
Normal file
71
storage.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package shortify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bucketName = []byte("shorturls")
|
||||||
|
|
||||||
|
func (s *Shortener) get(shortID string) (string, error) {
|
||||||
|
if val, ok := s.memStore.Load(shortID); ok {
|
||||||
|
s.cache.Add(shortID, val.(string))
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := s.cache.Get(shortID); ok {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var longURL string
|
||||||
|
err := s.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket(bucketName)
|
||||||
|
if b == nil {
|
||||||
|
return errors.New("not found")
|
||||||
|
}
|
||||||
|
v := b.Get([]byte(shortID))
|
||||||
|
if v != nil {
|
||||||
|
longURL = string(v)
|
||||||
|
s.cache.Add(shortID, longURL)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
return longURL, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) put(shortID, longURL string) error {
|
||||||
|
|
||||||
|
s.memStore.Store(shortID, longURL)
|
||||||
|
s.cache.Add(shortID, longURL)
|
||||||
|
|
||||||
|
// Queue write to DB (non-blocking)
|
||||||
|
select {
|
||||||
|
case s.writeChan <- [2]string{shortID, longURL}:
|
||||||
|
default:
|
||||||
|
log.Println("Warning: write queue full, short URL write may be dropped")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shortener) asyncDBWriter() {
|
||||||
|
for pair := range s.writeChan {
|
||||||
|
shortID := pair[0]
|
||||||
|
longURL := pair[1]
|
||||||
|
|
||||||
|
err := s.db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
b, err := tx.CreateBucketIfNotExists(bucketName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.Put([]byte(shortID), []byte(longURL))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("DB write failed for %s: %v", shortID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user