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:
parent
4ac7b1eb4b
commit
23842fd496
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
30
client/src/app/core/promises/mutex.ts
Normal file
30
client/src/app/core/promises/mutex.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
@ -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.")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user