-1

I am writing some asynchromous code in go which basically implements in-memory caching. I have a not very fast source which I query every minute (using ticker), and save the result into a cache struct field. This field can be queried from different goroutines asynchronously.

In order to avoid using mutexes when updating values from source I do not write to the same struct field which is being queried by other goroutines but create another variable, fill it and then assign it to the queried field. This works fine since assigning operation is atomic and no race occurs.

The code looks like the following:

// this fires up when cache is created
func (f *FeaturesCache) goStartUpdaterDaemon(ctx context.Context) {
    go func() {
        defer kiterrors.RecoverFunc(ctx, f.logger(ctx))

        ticker := time.NewTicker(updateFeaturesPeriod) // every minute
        defer ticker.Stop()

        for {
            select {
            case <-ticker.C:
                f.refill(ctx)
            case <-ctx.Done():
                return
            }
        }
    }()
}

func (f *FeaturesCache) refill(ctx context.Context) {
    var newSources map[string]FeatureData
    // some querying and processing logic

    // save new data for future queries
    f.features = newSources

}

Now I need to add another view of my data so I can also get it from cache. Basically that means adding one more struct field which will be queriad and filled in the same way the previous one (features) was.

I need these 2 views of my data to be in sync, so it is undesired to have, for example, new data in view 2 and old data in view 1 or the other way round.

So the only thing I need to change about refill is to add a new field, at first I did it this way:

func (f *FeaturesCache) refill(ctx context.Context) {
    var newSources map[string]FeatureData
    var anotherView map[string]DataView2
    // some querying and processing logic

    // save new data for future queries
    f.features = newSources      // line A
    f.anotherView = anotherView  // line B
}

However, for this code I'm wondering whether it satisfies my consistency requirements. I am worried that if the scheduler decides to interrupt the goroutine which runs refill between lines A nd B (check the code above) than I might get inconsistency between data views.

So I researched the problem. Many sources on the Internet say that the scheduler switches goroutines on syscalls and function calls. However, according to this answer https://stackoverflow.com/a/64113553/12702274 since go 1.14 there is an asynchronous preemtion mechanism in go scheduler which switches goroutines based on their running time in addition to previously checked signals. That makes me think that it is actually possible that refill goroutine can be interrupted between lines A and B.

Then I thought about surrounding those 2 assignments with mutex - lock before line A, unlock after line B. However, it seems to me that this doesn't change things much. The goroutine may still be interrupted between lines A and B and the data gets inconsistent. The only thing mutex achieves here is that 2 simultaneous refills do not conflict with each other which is actually impossible, because I run them in the same thread as timer. Thus it is useless here.

So, is there any way I can ensure atomicity for two consecutive assignments?

  • 2
    "This works fine since assigning operation is atomic and no race occurs." This is already incorrect. Why don't you want to use a (RW)Mutex? – Peter Feb 09 '23 at 09:52
  • @peter because then I have to deal with deletion logic. Some of existong keys are not present in new data from source. So I need to delete them. By assigning a new map old keys are deleted automatically. Also I think that updating to many keys one by one will take considerable time and slow down 'get' queries to cache – Daniel Richter Feb 09 '23 at 10:34
  • 2
    Creating new maps and then replacing the maps as a whole is totally reasonable. You still have to protect the assignment of the new maps with a (RW)Mutex. The lock duration would be very short. – Peter Feb 09 '23 at 11:25
  • 1
    An operation is not atomic unless you use atomic operations. While the underlying memory/cpu op may be atomic, you have no guarantee of visibility or ordering per the Go language memory model (you are writing Go, not machine code). Always synchronize concurrent access, and verify with the race detector. – JimB Feb 09 '23 at 12:31

1 Answers1

0

If I understand your concern correctly, you don't want to lock existing cached data while updating it(bec. it takes time to update, you want to be able to allow usage of existing cached data while updating it in another routine right ?). Also you want to make f.features and f.anotherView updates atomic. What about to take your data in a map[int]map[string]FeatureData and map[int]map[string]DataView2. Put the data to a new key each time and let the queries from this key(newSearchIndex).

Just tried to explain in code roughly(think below like pseudo code)

type FeaturesCache struct {
    mu             sync.RWMutex
    features       map[int8]map[string]FeatureData
    anotherView    map[int8]map[string]DataView2
    oldSearchIndex int8
    newSearchIndex int8
}

func (f *FeaturesCache) CreateNewIndex() int8 {
    f.mu.Lock()
    defer f.mu.Unlock()
    return (f.newSearchIndex + 1) % 16 // mod 16 could be change per your refill rate
}

func (f *FeaturesCache) SetNewIndex(newIndex int8) {
    f.mu.Lock()
    defer f.mu.Unlock()
    f.oldSearchIndex = f.newSearchIndex
    f.newSearchIndex = newIndex
}

func (f *FeaturesCache) refill(ctx context.Context) {
    var newSources map[string]FeatureData
    var anotherView map[string]DataView2
    // some querying and processing logic

    // save new data for future queries
    newSearchIndex := f.CreateNewIndex()
    f.features[newSearchIndex] = newSources
    f.anotherView[newSearchIndex] = anotherView
    f.SetNewIndex(newSearchIndex) //Let the queries to new cached datas after updating search Index
    f.features[f.oldSearchIndex] = nil
    f.anotherView[f.oldSearchIndex] = nil
}
emre_isler
  • 13
  • 4