How Route Table Renewal Enables Dynamic Updates in Fabrica-Kit
Fabrica-Kit implements route table renewal through a compare-and-expire mechanism where services periodically call RenewSelf to extend their entry's TTL only if they still own the record, enabling lock-free dynamic updates without rewriting the entire routing table.
Fabrica-Kit provides a sophisticated routing infrastructure that supports dynamic service discovery through automatic route table renewal. This mechanism allows running instances to extend their registration lifetime atomically, ensuring stale entries expire automatically while healthy services maintain their presence. The implementation relies on a read-only view combined with a renewal component that handles TTL management without requiring full record rewrites.
Understanding the Renewal Interface
The renewal capability is defined in router/routetable/routetable.go through the ReNewalRouteTable interface. This interface embeds the read-only API and adds two critical methods for self-maintenance:
type ReNewalRouteTable interface {
ReadOnlyRouteTable
RenewSelf(ctx context.Context, color string, key int64, value string) error
TTL() time.Duration
}
The RenewSelf method accepts a color (logical shard identifier), key (unique instance ID), and value (service address). It only succeeds when the stored value still matches the provided value, preventing stale services from accidentally extending a record that has already been taken over by another instance. The TTL method exposes the configured time-to-live duration for the entries.
Core Implementation Architecture
Located in router/routetable/renewal.go, the renewalRouteTable struct provides the concrete implementation of dynamic updates. It holds a reference to the underlying data store (Data) and a configurable TTL duration.
Configurable TTL Management
By default, route table entries live for 24 hours (defaultTTL). You can override this using the WithTTL functional option during initialization:
// WithTTL configures a custom TTL.
func WithTTL(dur time.Duration) Option {
return func(r *renewalRouteTable) {
if dur <= 0 {
dur = defaultTTL
}
r.ttl = dur
}
}
The Compare-and-Expire Pattern
When a service calls RenewSelf, the implementation forwards to the storage backend's ExpireIfSame method:
func (r *renewalRouteTable) RenewSelf(ctx context.Context, color string, uid int64, value string) error {
if err := r.data.ExpireIfSame(ctx, r.BuildKey(color, uid), value, r.ttl); err != nil {
return errors.WithMessage(err, "renewIfSame route table failed")
}
return nil
}
The BuildKey method generates deterministic keys using the format r_<name>_{<color>}_{<uid>}. The ExpireIfSame operation—implemented by the Redis backend in router/routetable/redis/redis.go—performs an atomic compare-and-expire: it verifies the stored value equals the provided value, and only then sets the new expiration timestamp.
Dynamic Update Flow
The route table renewal process follows a self-healing, lock-free pattern:
- Registration: A service registers itself via
masterRouteTable.SetorSetNxOrGet, storing its address with the configured TTL. - Periodic Renewal: The service calls
RenewSelfat regular intervals (typically shorter than the TTL). - Ownership Verification: The underlying storage checks if the current value matches the caller's value.
- TTL Extension: If values match, the expiration timestamp refreshes to
current_time + TTL. - Conflict Detection: If another instance has overwritten the entry,
ExpireIfSamefails, forcing the stale service to re-register or halt operations.
This design ensures that each instance only touches its own record while automatic cleanup removes dead instances after the TTL expires.
Practical Implementation Examples
Creating a Redis-Backed Renewal Table
Initialize a renewal route table with a custom 5-minute TTL:
import (
"time"
"github.com/go-pantheon/fabrica-kit/router/routetable"
rtredis "github.com/go-pantheon/fabrica-kit/router/routetable/redis"
"github.com/redis/go-redis/v9"
)
func newRenewalTable() routetable.ReNewalRouteTable {
client := redis.NewUniversalClient(&redis.UniversalOptions{
Addrs: []string{"localhost:6379"},
})
ds := rtredis.New(client)
return routetable.NewRenewalRouteTable(ds, "serviceA",
routetable.WithTTL(5*time.Minute))
}
Registering a Service Instance
Store the address with the TTL defined in the renewal component:
func registerSelf(rt routetable.RouteTable, uid int64, address string) error {
return rt.Set(context.Background(), "blue", uid, address)
}
Implementing Periodic Renewal
Run a background goroutine to keep the registration alive:
func keepAlive(rt routetable.ReNewalRouteTable, uid int64, address string) {
ticker := time.NewTicker(2 * time.Minute)
defer ticker.Stop()
for range ticker.C {
if err := rt.RenewSelf(context.Background(), "blue", uid, address); err != nil {
log.Printf("renewal failed: %v", err)
// Handle failure: re-register, alert, or shutdown
}
}
}
Complete Integration Example
Wire everything together in your main application:
func main() {
rt := newRenewalTable()
uid := int64(12345)
addr := "10.0.0.42:8080"
if err := rt.Set(context.Background(), "green", uid, addr); err != nil {
log.Fatalf("register failed: %v", err)
}
go keepAlive(rt, uid, addr)
select {} // Keep application running
}
Summary
- Route table renewal in Fabrica-Kit uses a
ReNewalRouteTableinterface defined inrouter/routetable/routetable.gothat extends read-only capabilities with self-renewal methods. - The
renewalRouteTableimplementation inrouter/routetable/renewal.goprovides configurable TTL management through theWithTTLoption, defaulting to 24 hours. - Atomic compare-and-expire operations via
ExpireIfSameensure that only the current owner of a route entry can extend its lifetime, preventing race conditions during service takeover. - Key generation follows the pattern
r_<name>_{<color>}_{<uid>}, creating deterministic storage keys for consistent lookups. - This architecture enables lock-free dynamic updates where services independently maintain their registrations without coordinating with other instances.
Frequently Asked Questions
What is the default TTL for route table entries?
The default TTL is 24 hours (defined as defaultTTL in router/routetable/renewal.go). You can customize this duration using the WithTTL functional option when constructing the renewal table, allowing intervals as short as seconds or as long as days depending on your service stability requirements.
How does RenewSelf prevent stale services from extending entries?
RenewSelf implements a compare-and-expire pattern through the underlying storage's ExpireIfSame method. Before extending the TTL, the system atomically verifies that the stored value matches the value provided by the caller. If another instance has overwritten the entry with a new address, the comparison fails and the renewal returns an error, preventing the stale service from extending the TTL.
What happens when route table renewal fails?
When RenewSelf returns an error, it indicates that either the entry no longer exists, the service no longer owns the record (another instance has registered), or the storage backend is unavailable. The calling service should treat this as a signal to either re-register itself using Set or SetNxOrGet, or gracefully shut down if it cannot reclaim its identity.
Which storage backends support the renewal mechanism?
The renewal mechanism relies on the Data interface, which requires an ExpireIfSame method for atomic compare-and-expire operations. The Redis backend in router/routetable/redis/redis.go provides a production-ready implementation of this interface. You can implement the Data interface for other storage systems (such as etcd or Consul) as long as they support atomic compare-and-swap or compare-and-expire operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →