07/03/2025 - GO
Bu örnekte API kimlik doğrulama tokenlerini depolamak ve gerçek son kullanma tarihlerinden önce otomatik olarak sonlandırmak için sync.Map kullanacağız. Bu şekilde, API çağrıları yaparken zaten süresi dolmuş tokenleri kullanmaktan kaçınmak için elimizden geleni yapacağız. Token süresi dolmuşsa, yenisini alırız ve tekrar depolarız. Bu, tokenlerin geri dönüştürüldüğü basit bir döngüdür. Buradaki asıl nokta, sync.Map'ın bu amaç için nasıl kullanıldığıdır, API istemcisinin nasıl kullanıldığı değil, bu nedenle memory
paketine odaklanmaya çalışın.
├── main.go
└── src
├── client
│ └── postcode
│ ├── client.go
│ └── model.go
├── model
│ └── client
│ └── location
│ └── address.go
└── storage
└── memory
├── memory.go
└── memory_test.go
Burada süresi dolan tokenlerin her 30 saniyede bir kontrol edildiğinden emin oluyoruz. Ayrıca, gerçek tokenlerin son kullanma zamanının 5 dakikadan daha erken olduğundan emin oluyoruz.
package main
import (
"context"
"log"
"time"
"random/src/client/postcode"
"random/src/storage/memory"
)
func main() {
ctx := context.Background()
storage := memory.New(ctx, memory.Config{
RecycleInterval: time.Second * 30,
})
client := postcode.Client{
Config: postcode.Config{
Address: "https://api.location.com",
ClientID: "client-d",
ClientSecret: "client-secret",
AuthTokenKey: "postcode_auth_token",
AuthTokenEarlyExpiry: time.Minute * 5,
},
StorageManager: storage,
}
for {
if adr, err := client.FindAddress(ctx, "PC 123"); err != nil {
log.Println(err)
} else {
log.Println(adr.Postcode)
}
time.Sleep(time.Second)
}
}
package postcode
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"random/src/model/client/location"
)
// storageManager exposes contract for the storage layer.
type storageManager interface {
Store(ctx context.Context, key string, val any, exp *time.Time)
Load(ctx context.Context, key string) (any, bool)
Delete(ctx context.Context, key string)
}
type Config struct {
// Address represents APIs base URL.
Address string
// ClientID represents client's authentication identifier.
ClientID string
// ClientSecret represents client's authentication secret.
ClientSecret string
// AuthTokenKey represents the key for the token in storage.
AuthTokenKey string
// AuthTokenEarlyExpiry is used to truncate a duration from the original
// expiry time of the token to force early expiry. e.g. 5 minutes
AuthTokenEarlyExpiry time.Duration
}
// Client provides functionality to interact with the Postcode API.
type Client struct {
Config Config
StorageManager storageManager
RoundTripper http.RoundTripper
}
// FindAddress finds an address for the given postcode. In case of 401 response,
// token is deleted from the storage in order to force obtaining a new one for
// the next call.
func (c Client) FindAddress(ctx context.Context, postcode string) (location.Address, error) {
token, err := c.authenticate(ctx)
if err != nil {
return location.Address{}, fmt.Errorf("unable to authenticate: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.Config.Address+"/api/v1/addresses/"+postcode, nil)
if err != nil {
return location.Address{}, fmt.Errorf("build request: %w", err)
}
req.Header.Add("Authorization", token.TokenType+" "+token.AccessToken)
res, err := c.RoundTripper.RoundTrip(req)
if err != nil {
return location.Address{}, fmt.Errorf("send request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
if res.StatusCode == http.StatusUnauthorized {
go c.StorageManager.Delete(ctx, c.Config.AuthTokenKey)
}
return location.Address{}, fmt.Errorf("status code: %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return location.Address{}, fmt.Errorf("read response body: %w", err)
}
var loc location.Address
if err := json.Unmarshal(body, &loc); err != nil {
return location.Address{}, fmt.Errorf("unmarshal response body: %w", err)
}
return loc, nil
}
// authenticate returns the auth token from the storage. If not found, API is
// authenticated, token is stored in storage for reuse then returned.
func (c Client) authenticate(ctx context.Context) (authToken, error) {
val, ok := c.StorageManager.Load(ctx, c.Config.AuthTokenKey)
if ok {
if token, ok := val.(authToken); ok {
return token, nil
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Config.Address+"/auth/v1", nil)
if err != nil {
return authToken{}, fmt.Errorf("build request: %w", err)
}
req.SetBasicAuth(c.Config.ClientID, c.Config.ClientSecret)
res, err := c.RoundTripper.RoundTrip(req)
if err != nil {
return authToken{}, fmt.Errorf("send request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return authToken{}, fmt.Errorf("status code: %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return authToken{}, fmt.Errorf("read response body: %w", err)
}
var token authToken
if err := json.Unmarshal(body, &token); err != nil {
return authToken{}, fmt.Errorf("unmarshal response body: %w", err)
}
expiry := time.Now().UTC().Add(time.Second * time.Duration(token.ExpiresIn)).Add(-c.Config.AuthTokenEarlyExpiry)
go c.StorageManager.Store(ctx, c.Config.AuthTokenKey, token, &expiry)
return token, nil
}
package postcode
type authToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
package location
type Address struct {
Postcode string `json:"postcode"`
Country string `json:"country"`
}
package memory
import (
"context"
"sync"
"time"
)
type Config struct {
// RecycleInterval sets the interval which helps delete expired or about to
// expire items. It is better to keep it reasonable shorter so deletion
// process runs as often as possible but not too often because each run
// creates a new goroutine which would consume unreasonable amount of system
// resources. e.g. 30 seconds
RecycleInterval time.Duration
}
// Memory provides functionality to either permanently or temporarily store
// items in memory. Temporary items are cleaned up at regular intervals which
// is defined within the `config` parameter.
type Memory struct {
config Config
items *sync.Map
temps *sync.Map
}
func New(ctx context.Context, config Config) Memory {
memory := Memory{
config: config,
items: &sync.Map{},
temps: &sync.Map{},
}
go memory.recycle(ctx)
return memory
}
// Store permanently stores a new item or overrides existing one. If the item
// has an expiry time, it is also stored in a temporary store which will be
// cleaned up within the `recycle` method.
func (m Memory) Store(_ context.Context, key string, val any, exp *time.Time) {
m.items.Store(key, val)
if exp != nil {
m.temps.Store(key, exp)
}
}
// Load returns an item if found otherwise returns boolean false which means
// item is not found.
func (m Memory) Load(_ context.Context, key string) (any, bool) {
return m.items.Load(key)
}
// Delete deletes items by their key.
func (m Memory) Delete(_ context.Context, key string) {
m.items.Delete(key)
m.temps.Delete(key)
}
// recycle runs at regular intervals to delete expired or near to expire items.
func (m Memory) recycle(ctx context.Context) {
tick := time.NewTicker(m.config.RecycleInterval)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
m.temps.Range(func(key, val any) bool {
expiry, ok := val.(*time.Time)
if !ok {
return false
}
if time.Now().UTC().Before(*expiry) {
return true
}
m.items.Delete(key)
m.temps.Delete(key)
return true
})
}
}
}
package memory
import (
"context"
"testing"
"time"
)
func Test_Memory_New(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
memory := New(ctx, Config{RecycleInterval: time.Nanosecond})
expiry := time.Now().Add(time.Millisecond * 10)
memory.Store(ctx, "key", "val", &expiry)
val, found := memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
time.Sleep(time.Millisecond * 50)
val, found = memory.items.Load("key")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
val, found = memory.temps.Load("key")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
cancel()
}
func Test_Memory_store(t *testing.T) {
ctx := context.Background()
memory := New(ctx, Config{RecycleInterval: time.Second})
t.Run("store with expiry", func(t *testing.T) {
expiry := time.Now().Add(time.Minute)
memory.Store(ctx, "key-1", "val", &expiry)
val, found := memory.items.Load("key-1")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key-1")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
})
t.Run("store without expiry", func(t *testing.T) {
memory.Store(ctx, "key-2", "val", nil)
val, found := memory.items.Load("key-2")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key-2")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
})
}
func Test_Memory_load(t *testing.T) {
var (
ctx = context.Background()
exp = time.Now().Add(time.Minute)
)
memory := New(ctx, Config{RecycleInterval: time.Second})
memory.Store(ctx, "key", "val", &exp)
val, found := memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
}
func Test_Memory_delete(t *testing.T) {
var (
ctx = context.Background()
exp = time.Now().Add(time.Minute)
)
memory := New(ctx, Config{RecycleInterval: time.Second})
memory.Store(ctx, "key", "val", &exp)
val, found := memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
memory.Delete(ctx, "key")
val, found = memory.items.Load("key")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
val, found = memory.temps.Load("key")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
}
func Test_Memory_recycle(t *testing.T) {
t.Run("exit as soon as context is cancelled and avoid deleting item", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Nanosecond)
t.Cleanup(func() {
cancel()
})
expiry := time.Now().Add(time.Minute)
memory := New(ctx, Config{RecycleInterval: time.Second})
memory.Store(ctx, "key", "val", &expiry)
val, found := memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
memory.recycle(ctx)
val, found = memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
})
t.Run("not expired item is not deleted", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond*10)
t.Cleanup(func() {
cancel()
})
expiry := time.Now().Add(time.Minute)
memory := New(ctx, Config{RecycleInterval: time.Nanosecond})
memory.Store(ctx, "key", "val", &expiry)
val, found := memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
memory.recycle(ctx)
val, found = memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
})
t.Run("expired item is deleted", func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond*20)
t.Cleanup(func() {
cancel()
})
expiry := time.Now().Add(time.Millisecond)
memory := New(ctx, Config{RecycleInterval: time.Nanosecond})
memory.Store(ctx, "key", "val", &expiry)
val, found := memory.items.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
val, found = memory.temps.Load("key")
if val == nil {
t.Error("expected not nil")
}
if !found {
t.Error("expected to find item")
}
memory.recycle(ctx)
val, found = memory.items.Load("key")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
val, found = memory.temps.Load("key")
if val != nil {
t.Error("expected nil")
}
if found {
t.Error("expected to not find item")
}
cancel()
})
}