Recover-strategy to detect an updated server without a reload

This commit is contained in:
FinnStutzenstein 2019-07-10 15:54:17 +02:00
parent 3f6fe28f35
commit 77dee0d977
11 changed files with 164 additions and 46 deletions

View File

@ -166,6 +166,7 @@ export class AutoupdateService {
* Does a full update: Requests all data from the server and sets the DS to the fresh data. * Does a full update: Requests all data from the server and sets the DS to the fresh data.
*/ */
public async doFullUpdate(): Promise<void> { public async doFullUpdate(): Promise<void> {
const oldChangeId = this.DS.maxChangeId;
const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {}); const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {});
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS); const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
@ -180,5 +181,7 @@ export class AutoupdateService {
await this.DS.set(allModels, response.to_change_id); await this.DS.set(allModels, response.to_change_id);
this.DSUpdateManager.commit(updateSlot); this.DSUpdateManager.commit(updateSlot);
console.log(`Full update done from ${oldChangeId} to ${response.to_change_id}`);
} }
} }

View File

@ -29,11 +29,6 @@ export class ConstantsService {
*/ */
private constants: Constants; private constants: Constants;
/**
* Flag, if the websocket connection is open.
*/
private websocketOpen = false;
/** /**
* Flag, if constants are requested, but the server hasn't send them yet. * Flag, if constants are requested, but the server hasn't send them yet.
*/ */
@ -54,18 +49,26 @@ export class ConstantsService {
if (this.pending) { if (this.pending) {
// send constants to subscribers that await constants. // send constants to subscribers that await constants.
this.pending = false; this.pending = false;
Object.keys(this.pendingSubject).forEach(key => { this.informSubjects();
this.pendingSubject[key].next(this.constants[key]);
});
} }
}); });
// We can request constants, if the websocket connection opens. // We can request constants, if the websocket connection opens.
websocketService.connectEvent.subscribe(() => { // On retries, the `refresh()` method is called by the OpenSlidesService, so
if (!this.websocketOpen && this.pending) { // here we do not need to take care about this.
websocketService.noRetryConnectEvent.subscribe(() => {
if (this.pending) {
this.websocketService.send('constants', {}); this.websocketService.send('constants', {});
} }
this.websocketOpen = true; });
}
/**
* Inform subjects about changes.
*/
private informSubjects(): void {
Object.keys(this.pendingSubject).forEach(key => {
this.pendingSubject[key].next(this.constants[key]);
}); });
} }
@ -81,7 +84,7 @@ export class ConstantsService {
if (!this.pending) { if (!this.pending) {
this.pending = true; this.pending = true;
// if the connection is open, we directly can send the request. // if the connection is open, we directly can send the request.
if (this.websocketOpen) { if (this.websocketService.isConnected) {
this.websocketService.send('constants', {}); this.websocketService.send('constants', {});
} }
} }
@ -91,4 +94,15 @@ export class ConstantsService {
return this.pendingSubject[key].asObservable() as Observable<T>; return this.pendingSubject[key].asObservable() as Observable<T>;
} }
} }
/**
* Refreshed the constants
*/
public async refresh(): Promise<void> {
if (!this.websocketService.isConnected) {
return;
}
this.constants = await this.websocketService.sendAndGetResponse('constants', {});
this.informSubjects();
}
} }

View File

@ -1,10 +1,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { take } from 'rxjs/operators';
import { ConstantsService } from './constants.service'; import { ConstantsService } from './constants.service';
import { AutoupdateService } from './autoupdate.service'; import { AutoupdateService } from './autoupdate.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
const MIGRATIONVERSION = 'MigrationVersion'; const DB_SCHEMA_VERSION = 'DbSchemaVersion';
/** /**
* Manages upgrading the DataStore, if the migration version from the server is higher than the current one. * Manages upgrading the DataStore, if the migration version from the server is higher than the current one.
@ -19,16 +21,28 @@ export class DataStoreUpgradeService {
* @param storageService * @param storageService
*/ */
public constructor( public constructor(
autoupdateService: AutoupdateService, private autoupdateService: AutoupdateService,
constantsService: ConstantsService, private constantsService: ConstantsService,
storageService: StorageService private storageService: StorageService
) { ) {
constantsService.get<number>(MIGRATIONVERSION).subscribe(async version => { this.checkForUpgrade();
const currentVersion = await storageService.get<number>(MIGRATIONVERSION);
await storageService.set(MIGRATIONVERSION, version);
if (currentVersion && currentVersion !== version) {
autoupdateService.doFullUpdate();
} }
});
public async checkForUpgrade(): Promise<boolean> {
const version = await this.constantsService
.get<string | number>(DB_SCHEMA_VERSION)
.pipe(take(1))
.toPromise();
console.log('DB schema version:', version);
const currentVersion = await this.storageService.get<string>(DB_SCHEMA_VERSION);
await this.storageService.set(DB_SCHEMA_VERSION, version);
const doUpgrade = version !== currentVersion;
if (doUpgrade) {
console.log(`DB schema version changed from ${currentVersion} to ${version}`);
await this.autoupdateService.doFullUpdate();
}
return doUpgrade;
} }
} }

View File

@ -6,6 +6,8 @@ import { OperatorService } from './operator.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { AutoupdateService } from './autoupdate.service'; import { AutoupdateService } from './autoupdate.service';
import { DataStoreService } from './data-store.service'; import { DataStoreService } from './data-store.service';
import { ConstantsService } from './constants.service';
import { DataStoreUpgradeService } from './data-store-upgrade.service';
/** /**
* Handles the bootup/showdown of this application. * Handles the bootup/showdown of this application.
@ -44,7 +46,9 @@ export class OpenSlidesService {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private router: Router, private router: Router,
private autoupdateService: AutoupdateService, private autoupdateService: AutoupdateService,
private DS: DataStoreService private DS: DataStoreService,
private constantsService: ConstantsService,
private dataStoreUpgradeService: DataStoreUpgradeService
) { ) {
// Handler that gets called, if the websocket connection reconnects after a disconnection. // Handler that gets called, if the websocket connection reconnects after a disconnection.
// There might have changed something on the server, so we check the operator, if he changed. // There might have changed something on the server, so we check the operator, if he changed.
@ -170,8 +174,24 @@ export class OpenSlidesService {
await this.reboot(); await this.reboot();
} else if (requestChanges) { } else if (requestChanges) {
// User is still the same, but check for missed autoupdates. // User is still the same, but check for missed autoupdates.
await this.recoverAfterReconnect();
}
}
}
/**
* The cache-refresh strategy, if there was an reconnect and the user didn't changed.
*/
private async recoverAfterReconnect(): Promise<void> {
// Reload constants to get either new one (in general) and especially
// the "DbSchemaVersion" one, to check, if the DB has changed (e.g. due
// to an update)
await this.constantsService.refresh();
// If the DB schema version didn't change, request normal changes.
// If so, then a full update is implicit triggered, so we do not need to to anything.
if (!(await this.dataStoreUpgradeService.checkForUpgrade())) {
this.autoupdateService.requestChanges(); this.autoupdateService.requestChanges();
} }
} }
} }
}

View File

@ -55,7 +55,7 @@ export class PingService {
// Connects the ping-pong mechanism to the opening and closing of the connection. // Connects the ping-pong mechanism to the opening and closing of the connection.
this.websocketService.closeEvent.subscribe(() => this.stopPing()); this.websocketService.closeEvent.subscribe(() => this.stopPing());
this.websocketService.connectEvent.subscribe(() => this.startPing()); this.websocketService.generalConnectEvent.subscribe(() => this.startPing());
if (this.websocketService.isConnected) { if (this.websocketService.isConnected) {
this.startPing(); this.startPing();
} }

View File

@ -51,7 +51,7 @@ export class ProjectorDataService {
}); });
}); });
this.websocketService.connectEvent.subscribe(() => this.updateProjectorDataSubscription()); this.websocketService.generalConnectEvent.subscribe(() => this.updateProjectorDataSubscription());
} }
/** /**

View File

@ -90,16 +90,29 @@ export class WebsocketService {
return this._retryReconnectEvent; return this._retryReconnectEvent;
} }
/**
* Subjects that will be called, if connect took place, but not a retry reconnect.
* THis is the complement from the generalConnectEvent to the retryReconnectEvent.
*/
private readonly _noRetryConnectEvent: EventEmitter<void> = new EventEmitter<void>();
/**
* Getter for the no-retry connect event.
*/
public get noRetryConnectEvent(): EventEmitter<void> {
return this._noRetryConnectEvent;
}
/** /**
* Listeners will be nofitied, if the wesocket connection is establiched. * Listeners will be nofitied, if the wesocket connection is establiched.
*/ */
private readonly _connectEvent: EventEmitter<void> = new EventEmitter<void>(); private readonly _generalConnectEvent: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Getter for the connect event. * Getter for the connect event.
*/ */
public get connectEvent(): EventEmitter<void> { public get generalConnectEvent(): EventEmitter<void> {
return this._connectEvent; return this._generalConnectEvent;
} }
/** /**
@ -234,12 +247,14 @@ export class WebsocketService {
return; return;
} }
this._connectionOpen = true;
if (retry) { if (retry) {
this.dismissConnectionErrorNotice(); this.dismissConnectionErrorNotice();
this._retryReconnectEvent.emit(); this._retryReconnectEvent.emit();
} else {
this._noRetryConnectEvent.emit();
} }
this._connectionOpen = true; this._generalConnectEvent.emit();
this._connectEvent.emit();
this.sendQueueWhileNotConnected.forEach(entry => { this.sendQueueWhileNotConnected.forEach(entry => {
this.websocket.send(entry); this.websocket.send(entry);
}); });

View File

@ -1,3 +1,5 @@
import hashlib
import logging
import os import os
import sys import sys
from collections import OrderedDict from collections import OrderedDict
@ -10,6 +12,9 @@ from django.db.models import Max
from django.db.models.signals import post_migrate, pre_delete from django.db.models.signals import post_migrate, pre_delete
logger = logging.getLogger("openslides.core")
class CoreAppConfig(AppConfig): class CoreAppConfig(AppConfig):
name = "openslides.core" name = "openslides.core"
verbose_name = "OpenSlides Core" verbose_name = "OpenSlides Core"
@ -60,9 +65,7 @@ class CoreAppConfig(AppConfig):
) )
post_migrate.connect( post_migrate.connect(
call_save_default_values, manage_config, sender=self, dispatch_uid="core_manage_config"
sender=self,
dispatch_uid="core_save_config_default_values",
) )
pre_delete.connect( pre_delete.connect(
autoupdate_for_many_to_many_relations, autoupdate_for_many_to_many_relations,
@ -175,17 +178,33 @@ class CoreAppConfig(AppConfig):
# get max migration id -> the "version" of the DB # get max migration id -> the "version" of the DB
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
constants["MigrationVersion"] = MigrationRecorder.Migration.objects.aggregate( migration_version = MigrationRecorder.Migration.objects.aggregate(Max("id"))[
Max("id") "id__max"
)["id__max"] ]
config_version = config["config_version"]
hash = hashlib.sha1(
f"{migration_version}#{config_version}".encode()
).hexdigest()
constants["DbSchemaVersion"] = hash
logger.info(f"DbSchemaVersion={hash}")
return constants return constants
def call_save_default_values(**kwargs): def manage_config(**kwargs):
"""
Should be run after every migration. Saves default values
of all non db-existing config objects into the db. Deletes all
unnecessary old config values, e.g. all db entries, that does
not have a config_variable anymore. Increments the config version,
if at least one of the operations altered some data.
"""
from .config import config from .config import config
config.save_default_values() altered = config.save_default_values()
altered = config.cleanup_old_config_values() or altered
if altered:
config.increment_version()
def startup(): def startup():
@ -201,6 +220,6 @@ def startup():
from openslides.utils.cache import element_cache from openslides.utils.cache import element_cache
from openslides.core.models import History from openslides.core.models import History
set_constants(get_constants_from_apps())
element_cache.ensure_cache() element_cache.ensure_cache()
set_constants(get_constants_from_apps())
History.objects.build_history() History.objects.build_history()

View File

@ -205,13 +205,14 @@ class ConfigHandler:
self.config_variables.update(item_index) self.config_variables.update(item_index)
def save_default_values(self) -> None: def save_default_values(self) -> bool:
""" """
Saves the default values to the database. Saves the default values to the database. Does also build the dictonary key_to_id.
Does also build the dictonary key_to_id. Returns True, if something in the DB was changed.
""" """
self.key_to_id = {} self.key_to_id = {}
altered_config = False
for item in self.config_variables.values(): for item in self.config_variables.values():
try: try:
db_value = ConfigStore.objects.get(key=item.name) db_value = ConfigStore.objects.get(key=item.name)
@ -220,7 +221,29 @@ class ConfigHandler:
db_value.key = item.name db_value.key = item.name
db_value.value = item.default_value db_value.value = item.default_value
db_value.save(skip_autoupdate=True) db_value.save(skip_autoupdate=True)
altered_config = True
self.key_to_id[db_value.key] = db_value.id self.key_to_id[db_value.key] = db_value.id
return altered_config
def increment_version(self) -> None:
"""
Increments the config key "config_version"
"""
db_value = ConfigStore.objects.get(key="config_version")
db_value.value = db_value.value + 1
db_value.save(skip_autoupdate=True)
def cleanup_old_config_values(self) -> bool:
"""
Deletes all config variable in the database, if the keys are not
in key_to_id. This required a fully build key_to_id!
Returns True, if something in the DB was changed.
"""
key_to_id = key_to_id = cast(Dict[str, int], self.key_to_id)
queryset = ConfigStore.objects.exclude(key__in=key_to_id.keys())
altered_config = queryset.exists()
queryset.delete()
return altered_config
def get_collection_string(self) -> str: def get_collection_string(self) -> str:
""" """

View File

@ -401,3 +401,12 @@ def get_config_variables():
weight=1000, weight=1000,
group="Custom translations", group="Custom translations",
) )
# Config version
yield ConfigVariable(
name="config_version",
input_type="integer",
default_value=1,
group="Version",
hidden=True,
)

View File

@ -55,9 +55,10 @@ def pytest_collection_modifyitems(items):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def constants(request): def constants(request, reset_cache):
""" """
Resets the constants on every test. Resets the constants on every test. The filled cache is needed to
build the constants, because some of them depends on the config.
Uses fake constants, if the db is not in use. Uses fake constants, if the db is not in use.
""" """