├── .gitignore ├── LICENSE ├── README.md ├── backup.go ├── carbon.go ├── clean.go ├── examples ├── StoringBasicDataTypes └── StoringStruct ├── go.mod ├── go.sum ├── models.go └── newStore.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | main.go 3 | /carbon 4 | /bin 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 scott-mescudi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carbon - A Simple In-Memory Cache for Quick Development 2 | 3 | [![Go Reference](https://pkg.go.dev/badge//github.com/scott-mescudi/carbon/.svg)](https://pkg.go.dev//github.com/scott-mescudi/carbon/) 4 | 5 | **Carbon** is a lightweight, high-performance in-memory cache library for Go, designed to be easy to use with seamless cache invalidation. 6 | 7 | At its core, Carbon leverages a thread-safe `sync.Map` to ensure simplicity and reliability. The cache operates locally, meaning only the application instance that initialized it can access the data, ensuring isolation and high performance. 8 | 9 | > **⚠ Disclaimer:** 10 | > **Carbon** was designed as a quick and simple caching solution for hackathons, prototypes, and non-critical applications. It is **not recommended for production use**, as it lacks advanced features like distributed caching, data replication, and fault tolerance. 11 | 12 | --- 13 | 14 | ### Key Invalidation 15 | Carbon provides two efficient mechanisms for invalidating expired keys: 16 | - **Background Expiry Check (Optional):** A configurable Go routine runs in the background, periodically checking for expired items and automatically removing them. 17 | - **On-Demand Expiry Check:** When retrieving a key, Carbon checks if it is expired. If expired, the key is deleted immediately, and `nil` is returned. 18 | 19 | ### Why Use Carbon? 20 | - **Simplicity and Expiry Management:** Carbon provides a straightforward in-memory cache solution with built-in support for key expiry and flexible invalidation strategies. 21 | - **Local Access Only:** Perfect for use cases where the cache doesn't need to be shared across multiple instances of your application. 22 | 23 | **When Not to Use Carbon:** 24 | If your cache needs to be accessible by multiple instances or services, Redis or a distributed caching solution would be a better fit. 25 | 26 | --- 27 | 28 | ## Installation 29 | 30 | To get started with **Carbon**, install it via `go get`: 31 | 32 | ```bash 33 | go get github.com/scott-mescudi/carbon 34 | ``` 35 | 36 | ## Basic Usage 37 | 38 | Carbon provides an easy-to-use in-memory caching solution with built-in expiry handling. Here's how to use it: 39 | 40 | ### Create a New Cache Store 41 | 42 | To create a new instance of the **CarbonStore**, use the `NewCarbonStore` function. You can optionally pass in a `cleanFrequency` to periodically clean expired items from the store. If you do not want the cleaner routine to run, pass `carbon.NoClean`. 43 | 44 | ```go 45 | package main 46 | 47 | import ( 48 | "fmt" 49 | "github.com/scott-mescudi/carbon" 50 | "time" 51 | ) 52 | 53 | func main() { 54 | // Create a new Carbon store with a 5-minute cleanup frequency 55 | cache := carbon.NewCarbonStore(5 * time.Minute) 56 | 57 | // Set a cache key with a 10-second expiry 58 | cache.Set("user:123", "John Doe", 10 * time.Second) 59 | 60 | // Retrieve a cached value 61 | value, err := cache.Get("user:123") 62 | if err != nil { 63 | fmt.Println("Error:", err) 64 | return 65 | } 66 | 67 | fmt.Println("Cached Value:", value) 68 | } 69 | ``` 70 | 71 | Alternatively, to create a store without the cleaner: 72 | 73 | ```go 74 | // Create a new Carbon store without the cleaner routine 75 | cache := carbon.NewCarbonStore(carbon.NoClean) 76 | ``` 77 | 78 | ### Importing a Cache Store from a File 79 | 80 | You can also initialize the **CarbonStore** by loading data from a file. The `ImportStoreFromFile` function parses a file, matches key-value pairs using a regular expression, and loads them into the cache. 81 | 82 | ```go 83 | cache, err := carbon.ImportStoreFromFile("cache_data.txt", 5 * time.Minute) 84 | if err != nil { 85 | fmt.Println("Error loading cache:", err) 86 | } else { 87 | fmt.Println("Cache loaded successfully!") 88 | } 89 | ``` 90 | 91 | ### Setting and Getting Cached Values 92 | 93 | You can store values in the cache with a specified expiration time. If the expiration time is not set, the key will persist indefinitely until it is manually deleted. 94 | 95 | #### Set a Cache Key 96 | 97 | You can set keys with the following options: 98 | - **Expiry time:** Specify a duration after which the key will expire. 99 | - **No expiry:** Use the `carbon.NoExpiry` flag to keep the key indefinitely. 100 | 101 | ```go 102 | // Set a cache key that expires in 10 seconds 103 | cache.Set("user:123", "John Doe", 10 * time.Second) 104 | 105 | // Set a cache key with no expiry (keeps the key forever, or until manually deleted) 106 | cache.Set("user:124", "Jane Doe", carbon.NoExpiry) 107 | 108 | // Set a cache key with no expiry and no cleaner 109 | cache.Set("user:125", "Alice", carbon.NoExpiry) 110 | ``` 111 | 112 | #### Get a Cache Key 113 | 114 | ```go 115 | value, err := cache.Get("user:123") 116 | if err != nil { 117 | fmt.Println("Error:", err) 118 | return 119 | } 120 | 121 | fmt.Println("Value:", value) 122 | ``` 123 | 124 | If the key is expired, it will be removed from the cache, and an error will be returned: 125 | 126 | ```go 127 | // If the key has expired 128 | value, err := cache.Get("user:123") 129 | if err != nil { 130 | fmt.Println("Error:", err) // Output: 'user:123' is expired 131 | } 132 | ``` 133 | 134 | ### Deleting Keys 135 | 136 | You can delete keys manually from the cache: 137 | 138 | ```go 139 | cache.Delete("user:123") 140 | ``` 141 | 142 | ### Clearing the Cache Store 143 | 144 | To clear all keys in the cache: 145 | 146 | ```go 147 | cache.ClearStore() 148 | ``` 149 | 150 | ### Stopping the Cache Cleaner 151 | 152 | The background cleaner is responsible for removing expired items periodically. If you want to stop the cleaner, you can call the `StopCleaner` method: 153 | 154 | ```go 155 | cache.StopCleaner() 156 | ``` 157 | 158 | ### Backup and Restore Cache 159 | 160 | To back up your cache to a file, use the `BackupToFile` method: 161 | 162 | ```go 163 | err := cache.BackupToFile("backup.txt") 164 | if err != nil { 165 | fmt.Println("Error backing up cache:", err) 166 | } else { 167 | fmt.Println("Cache backed up successfully!") 168 | } 169 | ``` 170 | 171 | To import the backup into a new **CarbonStore** instance, use `ImportStoreFromFile`. 172 | 173 | --- 174 | 175 | ## Key Invalidation Mechanisms 176 | 177 | Carbon provides two mechanisms for invalidating expired keys: 178 | 179 | - **Background Expiry Check (Optional):** A background Go routine runs at a configurable interval (`cleanFrequency`) and checks for expired items. Expired items are deleted automatically. 180 | 181 | Example of setting up the cleaner with a 5-minute interval: 182 | ```go 183 | cache := carbon.NewCarbonStore(5 * time.Minute) 184 | ``` 185 | 186 | - **On-Demand Expiry Check:** Each time you attempt to retrieve a cached value, Carbon will check if it is expired. If expired, the key is deleted, and `nil` is returned. 187 | 188 | --- 189 | 190 | ## Examples 191 | 192 | To see more use cases and examples of how to implement **Carbon** in your Go applications, check out the [examples folder](./examples). There, you'll find several example programs that demonstrate different features of the Carbon cache, such as basic caching, using a file-based store, key expiry, and more advanced features like cache backups and cleaner configurations. 193 | 194 | --- 195 | 196 | ## Todo 197 | - **Automatic Expiry Based on Events** 198 | Add event-driven expiry, where cache keys automatically expire when certain conditions or external events (e.g., database updates) are triggered. 199 | 200 | - **Cache Size Limit** 201 | Introduce a maximum cache size (either by memory or number of entries) with configurable eviction policies to automatically manage cache size, along with monitoring features to track cache performance. -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | package carbon 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // BackupToFile creates a backup file on disk with the given file name 9 | // and stores all the key-value pairs from the store in the file in the format {key=value}. 10 | // This function does not store the expiration times of the keys, only the values. 11 | func (s *CarbonStore) BackupToFile(BackupFileName string) error { 12 | f, err := os.Create(BackupFileName) 13 | if err != nil { 14 | return err 15 | } 16 | defer f.Close() 17 | 18 | s.store.Range(func(key, value any) bool { 19 | carb := value.(CarbonValue) 20 | _, err = fmt.Fprintf(f, "{%v=%v}", key, carb.Value) 21 | if err != nil { 22 | return false 23 | } 24 | return true 25 | }) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /carbon.go: -------------------------------------------------------------------------------- 1 | package carbon 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // NoExpiry is a constant used to indicate that the value should not expire. 10 | NoExpiry = -1 11 | // NoClean is a constant used to indicate that there is no cleaning operation set. 12 | NoClean = -1 13 | ) 14 | 15 | var ( 16 | CacheLimit = -1 17 | ) 18 | 19 | // Set adds a key-value pair to the store with an optional expiration time. 20 | // If the expiration is set to carbon.NoExpiry, the key-value pair will remain in the store indefinitely. 21 | func (s *CarbonStore) Set(key, value any, expiry time.Duration) { 22 | if expiry == NoExpiry { 23 | s.store.Store(key, CarbonValue{Value: value, Expiry: nil}) 24 | return 25 | } 26 | 27 | exp := time.Now().Add(expiry) 28 | s.store.Store(key, CarbonValue{Value: value, Expiry: &exp}) 29 | } 30 | 31 | // Get retrieves a value by its key from the store. 32 | // If the key does not exist, it returns nil and an error. 33 | // If the key is expired, it deletes the key and returns nil and an error. 34 | func (s *CarbonStore) Get(key any) (value any, err error) { 35 | v, ok := s.store.Load(key) 36 | if v == nil || !ok { 37 | return nil, fmt.Errorf("'%v' does not exist", key) 38 | } 39 | 40 | carb := v.(CarbonValue) 41 | if carb.Expiry != nil { 42 | if carb.Expiry.Before(time.Now()) { 43 | s.store.Delete(key) 44 | return nil, fmt.Errorf("'%v' is expired", key) 45 | } 46 | } 47 | 48 | return carb.Value, nil 49 | } 50 | 51 | // GetTTL retrieves the TTL of a value by its key from the store. 52 | // If the key does not exist, it returns nil and an error. 53 | // if there is no TTL set it retruns nil and a error 54 | func (s *CarbonStore) GetTTL(key any) (TTL *time.Time, err error) { 55 | v, ok := s.store.Load(key) 56 | if v == nil || !ok { 57 | return nil, fmt.Errorf("'%v' does not exist", key) 58 | } 59 | 60 | carb := v.(CarbonValue) 61 | if carb.Expiry == nil { 62 | return nil, fmt.Errorf("no TTL set") 63 | } 64 | 65 | return carb.Expiry, nil 66 | } 67 | 68 | // UpdateTTL updates the Time-To-Live (TTL) for a specified key in the CarbonStore. 69 | // It modifies the TTL of an existing key, effectively extending or reducing its expiration time. 70 | // If the key does not exist in the store, or if there is an issue updating the TTL, an error is returned. 71 | func (s *CarbonStore) UpdateTTL(key any, newTTL time.Duration) error { 72 | rawValue, ok := s.store.Load(key) 73 | if !ok || rawValue == nil { 74 | return fmt.Errorf("Failed to update TTL: key doesnt exist") 75 | } 76 | 77 | value := rawValue.(CarbonValue) 78 | nt := time.Now().Add(newTTL) 79 | value.Expiry = &nt 80 | 81 | _, ok = s.store.Swap(key, value) 82 | if !ok { 83 | return fmt.Errorf("Failed to update TTL") 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // CompareAndSwap swaps the old and new values for key 90 | // if the value stored in the map is equal to old. 91 | // The old value must be of the same type 92 | func (s *CarbonStore) CompareAndSwap(key, old, new any) (swapped bool) { 93 | TTL, err := s.GetTTL(key) 94 | if TTL == nil || err != nil { 95 | return false 96 | } 97 | 98 | sOld := CarbonValue{Value: old, Expiry: TTL} 99 | sNew := CarbonValue{Value: new, Expiry: TTL} 100 | 101 | return s.store.CompareAndSwap(key, sOld, sNew) 102 | } 103 | 104 | // Loops over the store and returns the amount of keys it contains. 105 | func (s *CarbonStore) Len() int { 106 | total := 0 107 | s.store.Range(func(key, value any) bool { 108 | total++ 109 | return true 110 | }) 111 | 112 | return total 113 | } 114 | 115 | // Delete removes a key-value pair from the store by its key. 116 | func (s *CarbonStore) Delete(key any) { 117 | s.store.Delete(key) 118 | } 119 | 120 | // ClearStore removes all keys and values from the store, freeing up memory. 121 | func (s *CarbonStore) ClearStore() { 122 | s.store.Clear() 123 | } 124 | 125 | // CloseStore stops any active cleaner goroutines (if present) and clears the store to free memory. 126 | func (s *CarbonStore) CloseStore() { 127 | s.StopCleaner() 128 | s.store.Clear() 129 | } 130 | 131 | // Printall prints all key-value pairs in the store to stdout. 132 | func (s *CarbonStore) Printall() { 133 | s.store.Range(func(key, value any) bool { 134 | fmt.Println(key, value) 135 | return true 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /clean.go: -------------------------------------------------------------------------------- 1 | package carbon 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // cleanStore periodically cleans up expired keys from the store based on the specified frequency. 8 | // The function runs in a separate goroutine and checks for expired keys at regular intervals defined by cleanFrequency. 9 | // It stops when the StopCleaner function is called (via the s.stopChan channel). 10 | func (s *CarbonStore) cleanStore(cleanFrequency time.Duration) { 11 | ticker := time.NewTicker(cleanFrequency) 12 | defer ticker.Stop() 13 | 14 | for { 15 | select { 16 | case <-s.stopChan: 17 | return 18 | case <-ticker.C: 19 | s.store.Range(func(key, value any) bool { 20 | if value.(CarbonValue).Expiry == nil { 21 | return true 22 | } 23 | 24 | if time.Since(*value.(CarbonValue).Expiry) > 0 { 25 | s.store.Delete(key) 26 | } 27 | 28 | return true 29 | }) 30 | } 31 | } 32 | } 33 | 34 | // StopCleaner stops the periodic cleaning of expired keys by closing the stop channel. 35 | // It effectively halts the cleanStore function's loop. 36 | func (s *CarbonStore) StopCleaner() { 37 | close(s.stopChan) 38 | } 39 | -------------------------------------------------------------------------------- /examples/StoringBasicDataTypes: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/scott-mescudi/carbon" 6 | ) 7 | 8 | func main() { 9 | // Create a new Carbon cache store without the background cleaner. 10 | cdb := carbon.NewCarbonStore(carbon.NoClean) 11 | 12 | // Store basic data types in the cache. 13 | 14 | // Caching a string value with no expiry. 15 | cdb.Set("greeting", "Hello, Carbon!", carbon.NoExpiry) 16 | 17 | // Caching an integer value with no expiry. 18 | cdb.Set("user_age", 30, carbon.NoExpiry) 19 | 20 | // Caching a float value with no expiry. 21 | cdb.Set("user_score", 98.6, carbon.NoExpiry) 22 | 23 | // Retrieve the string value. 24 | stringValue, err := cdb.Get("greeting") 25 | if err != nil { 26 | fmt.Println("Error retrieving string:", err) 27 | return 28 | } 29 | if stringValue == nil { 30 | fmt.Println("String value is nil.") 31 | return 32 | } 33 | 34 | // Note: For basic data types like strings, integers, and floats, 35 | // there is no need for type assertion. However, it is recommended 36 | // for safety to explicitly check the type to avoid potential issues 37 | // if the cache stores unexpected types. 38 | 39 | // Type assert the string value (recommended for safety). 40 | greeting, ok := stringValue.(string) 41 | if !ok { 42 | fmt.Println("Failed to assert string value.") 43 | return 44 | } 45 | fmt.Println("Greeting:", greeting) 46 | 47 | // Retrieve the integer value. 48 | intValue, err := cdb.Get("user_age") 49 | if err != nil { 50 | fmt.Println("Error retrieving integer:", err) 51 | return 52 | } 53 | if intValue == nil { 54 | fmt.Println("Integer value is nil.") 55 | return 56 | } 57 | 58 | // Type assert the integer value (recommended for safety). 59 | age, ok := intValue.(int) 60 | if !ok { 61 | fmt.Println("Failed to assert integer value.") 62 | return 63 | } 64 | fmt.Println("User Age:", age) 65 | 66 | // Retrieve the float value. 67 | floatValue, err := cdb.Get("user_score") 68 | if err != nil { 69 | fmt.Println("Error retrieving float:", err) 70 | return 71 | } 72 | if floatValue == nil { 73 | fmt.Println("Float value is nil.") 74 | return 75 | } 76 | 77 | // Type assert the float value (recommended for safety). 78 | score, ok := floatValue.(float64) 79 | if !ok { 80 | fmt.Println("Failed to assert float value.") 81 | return 82 | } 83 | fmt.Println("User Score:", score) 84 | } 85 | -------------------------------------------------------------------------------- /examples/StoringStruct: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/scott-mescudi/carbon" 6 | ) 7 | 8 | // Define a struct to be stored in the cache 9 | type myStruct struct { 10 | id int 11 | username string 12 | } 13 | 14 | func main() { 15 | // Create a new Carbon cache store without the background cleaner. 16 | cdb := carbon.NewCarbonStore(carbon.NoClean) 17 | 18 | // Create an instance of 'myStruct' to store in the cache. 19 | ms := myStruct{id: 1, username: "scott-mescudi"} 20 | 21 | // Store the struct in the cache under the key "user1" with no expiry time. 22 | cdb.Set("user1", ms, carbon.NoExpiry) 23 | 24 | // Retrieve the value associated with the "user1" key. 25 | value, err := cdb.Get("user1") 26 | if err != nil { 27 | // If there's an error while retrieving, print it and return. 28 | fmt.Println("Error retrieving value:", err) 29 | return 30 | } 31 | 32 | // Check if the retrieved value is nil to prevent panics. 33 | if value == nil { 34 | fmt.Println("Cache value is nil.") 35 | return 36 | } 37 | 38 | // Since the Get function returns a value of type 'any', 39 | // we need to check if it's not nil and then type assert it to our struct. 40 | castedValue, ok := value.(myStruct) 41 | if !ok { 42 | // If the type assertion fails, print an error message. 43 | fmt.Println("Failed to assert value to 'myStruct'") 44 | return 45 | } 46 | 47 | // Now we can safely access the fields of 'myStruct'. 48 | fmt.Println("ID:", castedValue.id, "Username:", castedValue.username) 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scott-mescudi/carbon 2 | 3 | go 1.24.0 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scott-mescudi/carbon/9f471ca7e6074e9be78a7abb24c864e7f6d9f11d/go.sum -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package carbon 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type CarbonStore struct { 9 | store sync.Map 10 | stopChan chan struct{} 11 | } 12 | 13 | type CarbonValue struct { 14 | Value any 15 | Expiry *time.Time 16 | } 17 | -------------------------------------------------------------------------------- /newStore.go: -------------------------------------------------------------------------------- 1 | package carbon 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // NewCarbonStore creates and returns a new CarbonStore instance. 11 | // If a valid `cleanFrequency` is provided, it starts a goroutine to periodically clean expired keys from the store. 12 | func NewCarbonStore(cleanFrequency time.Duration) *CarbonStore { 13 | z := make(chan struct{}) 14 | s := CarbonStore{store: sync.Map{}, stopChan: z} 15 | 16 | if cleanFrequency != NoClean { 17 | go s.cleanStore(cleanFrequency) 18 | } 19 | 20 | return &s 21 | } 22 | 23 | // ImportStoreFromFile reads a file and extracts key-value pairs in the format {key=value} using a regular expression. 24 | // The key-value pairs found in the file are then set in the store with no expiry time (NoExpiry) or a Default expiry time. 25 | // It also starts a cleaner goroutine if `cleanFrequency` is provided. 26 | func ImportStoreFromFile(filepath string, cleanFrequency time.Duration, defaultExpiry time.Duration) (*CarbonStore, error) { 27 | file, err := os.ReadFile(filepath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | z := make(chan struct{}) 33 | s := CarbonStore{store: sync.Map{}, stopChan: z} 34 | 35 | pattern := `\{(\w+)=(\w+)\}` 36 | re := regexp.MustCompile(pattern) 37 | matches := re.FindAllStringSubmatch(string(file), -1) 38 | 39 | var ss time.Duration 40 | if defaultExpiry == NoExpiry { 41 | ss = NoExpiry 42 | } else { 43 | ss = defaultExpiry 44 | } 45 | 46 | for _, match := range matches { 47 | if len(match) == 3 { 48 | s.Set(match[1], match[2], ss) 49 | } 50 | } 51 | 52 | if cleanFrequency != NoClean { 53 | go s.cleanStore(cleanFrequency) 54 | } 55 | 56 | return &s, nil 57 | } 58 | --------------------------------------------------------------------------------