Synchronize autoupdate code in the client

If autoupdates are too fast, the first one may not be fully executed. Especially when the maxChangeId is not yet updated, the second Autoupdate will trigger a refresh, because for the client it "lay in the future". This can be prevented by synchronizing the autoupdate-handling code with a mutex.
This commit is contained in:
FinnStutzenstein 2020-05-08 16:17:14 +02:00
parent 4ac7b1eb4b
commit 23842fd496
No known key found for this signature in database
GPG Key ID: 9042F605C6324654
5 changed files with 45 additions and 3 deletions

View File

@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
import { BaseModel } from '../../shared/models/base/base-model'; import { BaseModel } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from './collection-string-mapper.service'; import { CollectionStringMapperService } from './collection-string-mapper.service';
import { DataStoreService, DataStoreUpdateManagerService } from './data-store.service'; import { DataStoreService, DataStoreUpdateManagerService } from './data-store.service';
import { Mutex } from '../promises/mutex';
import { WebsocketService, WEBSOCKET_ERROR_CODES } from './websocket.service'; import { WebsocketService, WEBSOCKET_ERROR_CODES } from './websocket.service';
interface AutoupdateFormat { interface AutoupdateFormat {
@ -45,6 +46,7 @@ interface AutoupdateFormat {
providedIn: 'root' providedIn: 'root'
}) })
export class AutoupdateService { export class AutoupdateService {
private mutex = new Mutex();
/** /**
* Constructor to create the AutoupdateService. Calls the constructor of the parent class. * Constructor to create the AutoupdateService. Calls the constructor of the parent class.
* @param websocketService * @param websocketService
@ -79,11 +81,13 @@ export class AutoupdateService {
* Handles the change ids of all autoupdates. * Handles the change ids of all autoupdates.
*/ */
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> { private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
const unlock = await this.mutex.lock();
if (autoupdate.all_data) { if (autoupdate.all_data) {
await this.storeAllData(autoupdate); await this.storeAllData(autoupdate);
} else { } else {
await this.storePartialAutoupdate(autoupdate); await this.storePartialAutoupdate(autoupdate);
} }
unlock();
} }
/** /**

View File

@ -258,6 +258,7 @@ export class DataStoreUpdateManagerService {
private serveNextSlot(): void { private serveNextSlot(): void {
if (this.updateSlotRequests.length > 0) { if (this.updateSlotRequests.length > 0) {
console.warn('Concurrent update slots');
const request = this.updateSlotRequests.pop(); const request = this.updateSlotRequests.pop();
request.resolve(); request.resolve();
} }

View File

@ -0,0 +1,30 @@
/**
* A mutex as described in every textbook
*
* Usage:
* ```
* mutex = new Mutex(); // create e.g. as class member
*
* // Somewhere in the code to lock (must be async code!)
* const unlock = await this.mutex.lock()
* // ...the code to synchronize
* unlock()
* ```
*/
export class Mutex {
private mutex = Promise.resolve();
public lock(): PromiseLike<() => void> {
// this will capture the code-to-synchronize
let begin: (unlock: () => void) => void = () => {};
// All "requests" to execute code are chained in a promise-chain
this.mutex = this.mutex.then(() => {
return new Promise(begin);
});
return new Promise(res => {
begin = res;
});
}
}

View File

@ -8,7 +8,11 @@ from django.core.exceptions import ImproperlyConfigured
from typing_extensions import Protocol from typing_extensions import Protocol
from . import logging from . import logging
from .redis import read_only_redis_amount_replicas, use_redis from .redis import (
read_only_redis_amount_replicas,
read_only_redis_wait_timeout,
use_redis,
)
from .schema_version import SchemaVersion from .schema_version import SchemaVersion
from .utils import split_element_id, str_dict_to_bytes from .utils import split_element_id, str_dict_to_bytes
@ -452,11 +456,11 @@ class RedisCacheProvider:
raise e raise e
if not read_only and read_only_redis_amount_replicas is not None: if not read_only and read_only_redis_amount_replicas is not None:
reported_amount = await redis.wait( reported_amount = await redis.wait(
read_only_redis_amount_replicas, 1000 read_only_redis_amount_replicas, read_only_redis_wait_timeout
) )
if reported_amount != read_only_redis_amount_replicas: if reported_amount != read_only_redis_amount_replicas:
logger.warn( logger.warn(
f"WAIT reported {reported_amount} replicas of {read_only_redis_amount_replicas} requested!" f"WAIT reported {reported_amount} replicas of {read_only_redis_amount_replicas} requested after {read_only_redis_wait_timeout} ms!"
) )
return result return result

View File

@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
use_redis = False use_redis = False
use_read_only_redis = False use_read_only_redis = False
read_only_redis_amount_replicas = None read_only_redis_amount_replicas = None
read_only_redis_wait_timeout = None
try: try:
import aioredis import aioredis
@ -35,6 +36,8 @@ else:
read_only_redis_amount_replicas = getattr(settings, "AMOUNT_REPLICAS", 1) read_only_redis_amount_replicas = getattr(settings, "AMOUNT_REPLICAS", 1)
logger.info(f"AMOUNT_REPLICAS={read_only_redis_amount_replicas}") logger.info(f"AMOUNT_REPLICAS={read_only_redis_amount_replicas}")
read_only_redis_wait_timeout = getattr(settings, "WAIT_TIMEOUT", 1000)
logger.info(f"WAIT_TIMEOUT={read_only_redis_wait_timeout}")
else: else:
logger.info("Redis is not configured.") logger.info("Redis is not configured.")