[pkg-nagios-changes] [Git][nagios-team/pkg-icinga2][upstream] New upstream version 2.13.2

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Sat Nov 13 07:04:40 GMT 2021



Bas Couwenberg pushed to branch upstream at Debian Nagios Maintainer Group / pkg-icinga2


Commits:
ed7c657a by Bas Couwenberg at 2021-11-13T07:03:21+01:00
New upstream version 2.13.2
- - - - -


28 changed files:

- CHANGELOG.md
- ICINGA2_VERSION
- doc/02-installation.md
- lib/base/dictionary.cpp
- lib/base/tlsutility.cpp
- lib/base/tlsutility.hpp
- lib/icinga/command.ti
- lib/icinga/customvarobject.ti
- lib/icinga/host.ti
- lib/icinga/notification.cpp
- lib/icinga/notification.ti
- lib/icinga/service.ti
- lib/icinga/timeperiod.ti
- lib/icinga/user.ti
- lib/icinga/usergroup.cpp
- lib/icinga/usergroup.hpp
- lib/icingadb/icingadb-objects.cpp
- lib/icingadb/icingadb-stats.cpp
- lib/icingadb/icingadb-utility.cpp
- lib/icingadb/icingadb.cpp
- lib/icingadb/icingadb.hpp
- lib/icingadb/icingadb.ti
- lib/icingadb/redisconnection.cpp
- test/CMakeLists.txt
- test/base-dictionary.cpp
- tools/mkclass/class_lexer.ll
- tools/mkclass/classcompiler.cpp
- tools/mkclass/classcompiler.hpp


Changes:

=====================================
CHANGELOG.md
=====================================
@@ -7,6 +7,32 @@ documentation before upgrading to a new release.
 
 Released closed milestones can be found on [GitHub](https://github.com/Icinga/icinga2/milestones?state=closed).
 
+## 2.13.2 (2021-11-12)
+
+This version only includes changes needed for the release of Icinga DB 1.0.0 RC2 and doesn't include any other bugfixes or features.
+
+### Icinga DB
+
+* Prefix command_id with command type #9085
+* Decouple environment from Icinga 2 Environment constant #9082
+* Make icinga:history:stream:*#event_id deterministic #9076
+* Add downtime.duration & service_state.host_id to Redis #9084
+* Sync checkables along with their states first #9081
+* Flush both buffered states and state checksums on initial dump #9079
+* Introduce icinga:history:stream:downtime#scheduled_by #9080
+* Actually write parent to parent_id of zones #9078
+* Set value in milliseconds for program_start in stats/heartbeat #9077
+* Clean up vanished objects from icinga:checksum:*:state #9074
+* Remove usernotification history stream #9073
+* Write IDs of notified users into notification history stream #9071
+* Make CheckResult#scheduling_source available to Icinga DB #9072
+* Stream runtime state updates only to icinga:runtime:state #9068
+* Publish Redis schema version via XADD icinga:schema #9069
+* Don't include checkable types in history IDs #9070
+* Remove unused Redis key 'icinga:zone:parent' #9075
+* Make sure object relationships are handled correctly during runtime updates #9089
+* Only log queries at debug level #9088
+
 ## 2.13.1 (2021-08-19)
 
 The main focus of this version is a security vulnerability in the TLS certificate verification of our metrics writers ElasticsearchWriter, GelfWriter, InfluxdbWriter and Influxdb2Writer.
@@ -500,6 +526,19 @@ Thanks to all contributors:
   * Code quality fixes
   * Small documentation fixes
 
+## 2.11.11 (2021-08-19)
+
+The main focus of these versions is a security vulnerability in the TLS certificate verification of our metrics writers ElasticsearchWriter, GelfWriter and InfluxdbWriter.
+
+### Security
+
+* Add TLS server certificate validation to ElasticsearchWriter, GelfWriter and InfluxdbWriter
+
+Depending on your setup, manual intervention beyond installing the new versions
+may be required, so please read the more detailed information in the
+[release blog post](https://icinga.com/blog/2021/08/19/icinga-2-13-1-security-release//)
+carefully
+
 ## 2.11.10 (2021-07-15)
 
 Version 2.11.10 fixes two security vulnerabilities that may lead to privilege


=====================================
ICINGA2_VERSION
=====================================
@@ -1,2 +1,2 @@
-Version: 2.13.1
+Version: 2.13.2
 Revision: 1


=====================================
doc/02-installation.md
=====================================
@@ -151,7 +151,7 @@ CentOS 8 additionally needs the PowerTools repository for EPEL:
 
 ```bash
 dnf install 'dnf-command(config-manager)'
-dnf config-manager --set-enabled PowerTools
+dnf config-manager --set-enabled powertools
 
 dnf install epel-release
 ```


=====================================
lib/base/dictionary.cpp
=====================================
@@ -235,10 +235,10 @@ Object::Ptr Dictionary::Clone() const
 }
 
 /**
- * Returns an array containing all keys
+ * Returns an ordered vector containing all keys
  * which are currently set in this directory.
  *
- * @returns an array of key names
+ * @returns an ordered vector of key names
  */
 std::vector<String> Dictionary::GetKeys() const
 {


=====================================
lib/base/tlsutility.cpp
=====================================
@@ -844,15 +844,7 @@ String SHA1(const String& s, bool binary)
 	if (binary)
 		return String(reinterpret_cast<const char*>(digest), reinterpret_cast<const char *>(digest + SHA_DIGEST_LENGTH));
 
-	static const char hexdigits[] = "0123456789abcdef";
-	char output[SHA_DIGEST_LENGTH*2+1];
-	for (int i = 0; i < SHA_DIGEST_LENGTH; i++) {
-		output[2*i] = hexdigits[digest[i] >> 4];
-		output[2*i + 1] = hexdigits[digest[i] & 0xf];
-	}
-	output[2*SHA_DIGEST_LENGTH] = 0;
-
-	return output;
+	return BinaryToHex(digest, SHA_DIGEST_LENGTH);
 }
 
 String SHA256(const String& s)
@@ -930,6 +922,18 @@ String RandomString(int length)
 	return result;
 }
 
+String BinaryToHex(const unsigned char* data, size_t length) {
+	static const char hexdigits[] = "0123456789abcdef";
+
+	String output(2*length, 0);
+	for (int i = 0; i < SHA_DIGEST_LENGTH; i++) {
+		output[2 * i] = hexdigits[data[i] >> 4];
+		output[2 * i + 1] = hexdigits[data[i] & 0xf];
+	}
+
+	return output;
+}
+
 bool VerifyCertificate(const std::shared_ptr<X509> &caCertificate, const std::shared_ptr<X509> &certificate, const String& crlFile)
 {
 	X509_STORE *store = X509_STORE_new();


=====================================
lib/base/tlsutility.hpp
=====================================
@@ -61,6 +61,7 @@ String PBKDF2_SHA256(const String& password, const String& salt, int iterations)
 String SHA1(const String& s, bool binary = false);
 String SHA256(const String& s);
 String RandomString(int length);
+String BinaryToHex(const unsigned char* data, size_t length);
 
 bool VerifyCertificate(const std::shared_ptr<X509>& caCertificate, const std::shared_ptr<X509>& certificate, const String& crlFile);
 bool IsCa(const std::shared_ptr<X509>& cacert);


=====================================
lib/icinga/command.ti
=====================================
@@ -11,11 +11,11 @@ namespace icinga
 abstract class Command : CustomVarObject
 {
 	[config] Value command (CommandLine);
-	[config] Value arguments;
+	[config, signal_with_old_value] Value arguments;
 	[config] int timeout {
 		default {{{ return 60; }}}
 	};
-	[config] Dictionary::Ptr env;
+	[config, signal_with_old_value] Dictionary::Ptr env;
 	[config, required] Function::Ptr execute;
 };
 


=====================================
lib/icinga/customvarobject.ti
=====================================
@@ -9,7 +9,7 @@ namespace icinga
 
 abstract class CustomVarObject : ConfigObject
 {
-	[config] Dictionary::Ptr vars;
+	[config, signal_with_old_value] Dictionary::Ptr vars;
 };
 
 }


=====================================
lib/icinga/host.ti
=====================================
@@ -15,7 +15,7 @@ class Host : Checkable
 	load_after Endpoint;
 	load_after Zone;
 
-	[config, no_user_modify, required] array(name(HostGroup)) groups {
+	[config, no_user_modify, required, signal_with_old_value] array(name(HostGroup)) groups {
 		default {{{ return new Array(); }}}
 	};
 


=====================================
lib/icinga/notification.cpp
=====================================
@@ -133,6 +133,9 @@ void Notification::Start(bool runtimeCreated)
 	if (ApiListener::IsHACluster() && GetNextNotification() < Utility::GetTime() + 60)
 		SetNextNotification(Utility::GetTime() + 60, true);
 
+	for (const UserGroup::Ptr& group : GetUserGroups())
+		group->AddNotification(this);
+
 	ObjectImpl<Notification>::Start(runtimeCreated);
 }
 
@@ -144,6 +147,9 @@ void Notification::Stop(bool runtimeRemoved)
 
 	if (obj)
 		obj->UnregisterNotification(this);
+
+	for (const UserGroup::Ptr& group : GetUserGroups())
+		group->RemoveNotification(this);
 }
 
 Checkable::Ptr Notification::GetCheckable() const


=====================================
lib/icinga/notification.ti
=====================================
@@ -36,8 +36,8 @@ class Notification : CustomVarObject < NotificationNameComposer
 			return TimePeriod::GetByName(GetPeriodRaw());
 		}}}
 	};
-	[config, protected] array(name(User)) users (UsersRaw);
-	[config, protected] array(name(UserGroup)) user_groups (UserGroupsRaw);
+	[config, signal_with_old_value] array(name(User)) users (UsersRaw);
+	[config, signal_with_old_value] array(name(UserGroup)) user_groups (UserGroupsRaw);
 	[config] Dictionary::Ptr times;
 	[config] array(Value) types;
 	[no_user_view, no_user_modify] int type_filter_real (TypeFilter);


=====================================
lib/icinga/service.ti
=====================================
@@ -27,7 +27,7 @@ class Service : Checkable < ServiceNameComposer
 	load_after Host;
 	load_after Zone;
 
-	[config, no_user_modify, required] array(name(ServiceGroup)) groups {
+	[config, no_user_modify, required, signal_with_old_value] array(name(ServiceGroup)) groups {
 		default {{{ return new Array(); }}}
 	};
 


=====================================
lib/icinga/timeperiod.ti
=====================================
@@ -18,15 +18,15 @@ class TimePeriod : CustomVarObject
 				return m_DisplayName.m_Value;
 		}}}
 	};
-	[config] Dictionary::Ptr ranges;
+	[config, signal_with_old_value] Dictionary::Ptr ranges;
 	[config, required] Function::Ptr update;
 	[config] bool prefer_includes {
 		default {{{ return true; }}}
 	};
-	[config, required] array(name(TimePeriod)) excludes {
+	[config, required, signal_with_old_value] array(name(TimePeriod)) excludes {
 		default {{{ return new Array(); }}}
 	};
-	[config, required] array(name(TimePeriod)) includes {
+	[config, required, signal_with_old_value] array(name(TimePeriod)) includes {
 		default {{{ return new Array(); }}}
 	};
 	[state, no_user_modify] Value valid_begin;


=====================================
lib/icinga/user.ti
=====================================
@@ -19,7 +19,7 @@ class User : CustomVarObject
 				return m_DisplayName.m_Value;
 		}}}
 	};
-	[config, no_user_modify, required] array(name(UserGroup)) groups {
+	[config, no_user_modify, required, signal_with_old_value] array(name(UserGroup)) groups {
 		default {{{ return new Array(); }}}
 	};
 	[config, navigation] name(TimePeriod) period (PeriodRaw) {


=====================================
lib/icinga/usergroup.cpp
=====================================
@@ -76,6 +76,24 @@ void UserGroup::RemoveMember(const User::Ptr& user)
 	m_Members.erase(user);
 }
 
+std::set<Notification::Ptr> UserGroup::GetNotifications() const
+{
+	std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+	return m_Notifications;
+}
+
+void UserGroup::AddNotification(const Notification::Ptr& notification)
+{
+	std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+	m_Notifications.insert(notification);
+}
+
+void UserGroup::RemoveNotification(const Notification::Ptr& notification)
+{
+	std::unique_lock<std::mutex> lock(m_UserGroupMutex);
+	m_Notifications.erase(notification);
+}
+
 bool UserGroup::ResolveGroupMembership(const User::Ptr& user, bool add, int rstack) {
 
 	if (add && rstack > 20) {


=====================================
lib/icinga/usergroup.hpp
=====================================
@@ -11,6 +11,7 @@ namespace icinga
 {
 
 class ConfigItem;
+class Notification;
 
 /**
  * An Icinga user group.
@@ -27,6 +28,10 @@ public:
 	void AddMember(const User::Ptr& user);
 	void RemoveMember(const User::Ptr& user);
 
+	std::set<intrusive_ptr<Notification>> GetNotifications() const;
+	void AddNotification(const intrusive_ptr<Notification>& notification);
+	void RemoveNotification(const intrusive_ptr<Notification>& notification);
+
 	bool ResolveGroupMembership(const User::Ptr& user, bool add = true, int rstack = 0);
 
 	static void EvaluateObjectRules(const User::Ptr& user);
@@ -34,6 +39,7 @@ public:
 private:
 	mutable std::mutex m_UserGroupMutex;
 	std::set<User::Ptr> m_Members;
+	std::set<intrusive_ptr<Notification>> m_Notifications;
 
 	static bool EvaluateObjectRule(const User::Ptr& user, const intrusive_ptr<ConfigItem>& group);
 };


=====================================
lib/icingadb/icingadb-objects.cpp
=====================================
@@ -15,6 +15,7 @@
 #include "base/array.hpp"
 #include "base/exception.hpp"
 #include "base/utility.hpp"
+#include "base/object-packer.hpp"
 #include "icinga/command.hpp"
 #include "icinga/compatutility.hpp"
 #include "icinga/customvarobject.hpp"
@@ -35,6 +36,7 @@
 #include <mutex>
 #include <set>
 #include <utility>
+#include <type_traits>
 
 using namespace icinga;
 
@@ -44,18 +46,23 @@ INITIALIZE_ONCE(&IcingaDB::ConfigStaticInitialize);
 
 std::vector<Type::Ptr> IcingaDB::GetTypes()
 {
+	// The initial config sync will queue the types in the following order.
 	return {
-		CheckCommand::TypeInstance,
-		Comment::TypeInstance,
+		// Sync them first to get their states ASAP.
+		Host::TypeInstance,
+		Service::TypeInstance,
+
+		// Then sync them for similar reasons.
 		Downtime::TypeInstance,
+		Comment::TypeInstance,
+
+		HostGroup::TypeInstance,
+		ServiceGroup::TypeInstance,
+		CheckCommand::TypeInstance,
 		Endpoint::TypeInstance,
 		EventCommand::TypeInstance,
-		Host::TypeInstance,
-		HostGroup::TypeInstance,
 		Notification::TypeInstance,
 		NotificationCommand::TypeInstance,
-		Service::TypeInstance,
-		ServiceGroup::TypeInstance,
 		TimePeriod::TypeInstance,
 		User::TypeInstance,
 		UserGroup::TypeInstance,
@@ -122,10 +129,47 @@ void IcingaDB::ConfigStaticInitialize()
 	Service::OnHostProblemChanged.connect([](const Service::Ptr& service, const CheckResult::Ptr&, const MessageOrigin::Ptr&) {
 		IcingaDB::StateChangeHandler(service);
 	});
+
+	Notification::OnUsersRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) {
+		IcingaDB::NotificationUsersChangedHandler(notification, oldValues, newValues);
+	});
+	Notification::OnUserGroupsRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) {
+		IcingaDB::NotificationUserGroupsChangedHandler(notification, oldValues, newValues);
+	});
+	TimePeriod::OnRangesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+		IcingaDB::TimePeriodRangesChangedHandler(timeperiod, oldValues, newValues);
+	});
+	TimePeriod::OnIncludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+		IcingaDB::TimePeriodIncludesChangedHandler(timeperiod, oldValues, newValues);
+	});
+	TimePeriod::OnExcludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) {
+		IcingaDB::TimePeriodExcludesChangedHandler(timeperiod, oldValues, newValues);
+	});
+	User::OnGroupsChangedWithOldValue.connect([](const User::Ptr& user, const Value& oldValues, const Value& newValues) {
+		IcingaDB::UserGroupsChangedHandler(user, oldValues, newValues);
+	});
+	Host::OnGroupsChangedWithOldValue.connect([](const Host::Ptr& host, const Value& oldValues, const Value& newValues) {
+		IcingaDB::HostGroupsChangedHandler(host, oldValues, newValues);
+	});
+	Service::OnGroupsChangedWithOldValue.connect([](const Service::Ptr& service, const Value& oldValues, const Value& newValues) {
+		IcingaDB::ServiceGroupsChangedHandler(service, oldValues, newValues);
+	});
+	Command::OnEnvChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) {
+		IcingaDB::CommandEnvChangedHandler(command, oldValues, newValues);
+	});
+	Command::OnArgumentsChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) {
+		IcingaDB::CommandArgumentsChangedHandler(command, oldValues, newValues);
+	});
+	CustomVarObject::OnVarsChangedWithOldValue.connect([](const ConfigObject::Ptr& object, const Value& oldValues, const Value& newValues) {
+		IcingaDB::CustomVarsChangedHandler(object, oldValues, newValues);
+	});
 }
 
 void IcingaDB::UpdateAllConfigObjects()
 {
+	m_Rcon->Sync();
+	m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "4"}, Prio::Heartbeat);
+
 	Log(LogInformation, "IcingaDB") << "Starting initial config/status dump";
 	double startTime = Utility::GetTime();
 
@@ -310,8 +354,10 @@ void IcingaDB::UpdateAllConfigObjects()
 				}
 			}
 
-			if (states.size() > 2)
+			if (states.size() > 2) {
 				transaction.emplace_back(std::move(states));
+				transaction.emplace_back(std::move(statesChksms));
+			}
 
 			if (transaction.size() > 1) {
 				transaction.push_back({"EXEC"});
@@ -533,12 +579,11 @@ std::vector<String> IcingaDB::GetTypeOverwriteKeys(const String& type)
 	if (type == "host" || type == "service" || type == "user") {
 		keys.emplace_back(m_PrefixConfigObject + type + "group:member");
 		keys.emplace_back(m_PrefixConfigObject + type + ":state");
+		keys.emplace_back(m_PrefixConfigCheckSum + type + ":state");
 	} else if (type == "timeperiod") {
 		keys.emplace_back(m_PrefixConfigObject + type + ":override:include");
 		keys.emplace_back(m_PrefixConfigObject + type + ":override:exclude");
 		keys.emplace_back(m_PrefixConfigObject + type + ":range");
-	} else if (type == "zone") {
-		keys.emplace_back(m_PrefixConfigObject + type + ":parent");
 	} else if (type == "notification") {
 		keys.emplace_back(m_PrefixConfigObject + type + ":user");
 		keys.emplace_back(m_PrefixConfigObject + type + ":usergroup");
@@ -571,8 +616,6 @@ std::vector<String> IcingaDB::GetTypeDumpSignalKeys(const Type::Ptr& type)
 		keys.emplace_back(m_PrefixConfigObject + lcType + ":override:include");
 		keys.emplace_back(m_PrefixConfigObject + lcType + ":override:exclude");
 		keys.emplace_back(m_PrefixConfigObject + lcType + ":range");
-	} else if (type == Zone::TypeInstance) {
-		keys.emplace_back(m_PrefixConfigObject + lcType + ":parent");
 	} else if (type == Notification::TypeInstance) {
 		keys.emplace_back(m_PrefixConfigObject + lcType + ":user");
 		keys.emplace_back(m_PrefixConfigObject + lcType + ":usergroup");
@@ -595,20 +638,14 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate)
 {
 	String objectKey = GetObjectIdentifier(object);
-	String objectKeyName;
+	String objectKeyName = typeName + "_id";
 
 	Type::Ptr type = object->GetReflectionType();
-	if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
-		objectKeyName = "command_id";
-	} else {
-		objectKeyName = typeName + "_id";
-	}
 
 	CustomVarObject::Ptr customVarObject = dynamic_pointer_cast<CustomVarObject>(object);
-	auto env (GetEnvironment());
 
 	if (customVarObject) {
-		auto vars(SerializeVars(customVarObject));
+		auto vars(SerializeVars(customVarObject->GetVars()));
 		if (vars) {
 			auto& typeCvs (hMSets[m_PrefixConfigObject + typeName + ":customvar"]);
 			auto& allCvs (hMSets[m_PrefixConfigObject + "customvar"]);
@@ -628,7 +665,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 					}
 				}
 
-				String id = HashValue(new Array(Prepend(env, Prepend(kv.first, GetObjectIdentifiersWithoutEnv(object)))));
+				String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
 				typeCvs.emplace_back(id);
 
 				Dictionary::Ptr	data = new Dictionary({{objectKeyName, objectKey}, {"environment_id", m_EnvironmentId}, {"customvar_id", kv.first}});
@@ -650,7 +687,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		if (!actionUrl.IsEmpty()) {
 			auto& actionUrls (hMSets[m_PrefixConfigObject + "action:url"]);
 
-			auto id (HashValue(new Array({env, actionUrl})));
+			auto id (HashValue(new Array({m_EnvironmentId, actionUrl})));
 
 			if (runtimeUpdate || m_DumpedGlobals.ActionUrl.IsNew(id)) {
 				actionUrls.emplace_back(std::move(id));
@@ -665,7 +702,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		if (!notesUrl.IsEmpty()) {
 			auto& notesUrls (hMSets[m_PrefixConfigObject + "notes:url"]);
 
-			auto id (HashValue(new Array({env, notesUrl})));
+			auto id (HashValue(new Array({m_EnvironmentId, notesUrl})));
 
 			if (runtimeUpdate || m_DumpedGlobals.NotesUrl.IsNew(id)) {
 				notesUrls.emplace_back(std::move(id));
@@ -680,7 +717,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		if (!iconImage.IsEmpty()) {
 			auto& iconImages (hMSets[m_PrefixConfigObject + "icon:image"]);
 
-			auto id (HashValue(new Array({env, iconImage})));
+			auto id (HashValue(new Array({m_EnvironmentId, iconImage})));
 
 			if (runtimeUpdate || m_DumpedGlobals.IconImage.IsNew(id)) {
 				iconImages.emplace_back(std::move(id));
@@ -718,7 +755,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 			for (auto& group : groups) {
 				auto groupObj ((*getGroup)(group));
 				String groupId = GetObjectIdentifier(groupObj);
-				String id = HashValue(new Array(Prepend(env, Prepend(GetObjectIdentifiersWithoutEnv(groupObj), GetObjectIdentifiersWithoutEnv(object)))));
+				String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()}));
 				members.emplace_back(id);
 				Dictionary::Ptr data = new Dictionary({{objectKeyName, objectKey}, {"environment_id", m_EnvironmentId}, {typeName + "group_id", groupId}});
 				members.emplace_back(JsonEncode(data));
@@ -746,10 +783,10 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 			rangeIds->Reserve(ranges->GetLength());
 
 			for (auto& kv : ranges) {
-				String rangeId = HashValue(new Array({env, kv.first, kv.second}));
+				String rangeId = HashValue(new Array({m_EnvironmentId, kv.first, kv.second}));
 				rangeIds->Add(rangeId);
 
-				String id = HashValue(new Array(Prepend(env, Prepend(kv.first, Prepend(kv.second, GetObjectIdentifiersWithoutEnv(object))))));
+				String id = HashValue(new Array({m_EnvironmentId, kv.first, kv.second, object->GetName()}));
 				typeRanges.emplace_back(id);
 				Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"range_key", kv.first}, {"range_value", kv.second}});
 				typeRanges.emplace_back(JsonEncode(data));
@@ -779,7 +816,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 			String includeId = GetObjectIdentifier(includeTp);
 			includeChecksums->Add(includeId);
 
-			String id = HashValue(new Array(Prepend(env, Prepend(GetObjectIdentifiersWithoutEnv(includeTp), GetObjectIdentifiersWithoutEnv(object)))));
+			String id = HashValue(new Array({m_EnvironmentId, includeTp->GetName(), object->GetName()}));
 			includs.emplace_back(id);
 			Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"include_id", includeId}});
 			includs.emplace_back(JsonEncode(data));
@@ -809,7 +846,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 			String excludeId = GetObjectIdentifier(excludeTp);
 			excludeChecksums->Add(excludeId);
 
-			String id = HashValue(new Array(Prepend(env, Prepend(GetObjectIdentifiersWithoutEnv(excludeTp), GetObjectIdentifiersWithoutEnv(object)))));
+			String id = HashValue(new Array({m_EnvironmentId, excludeTp->GetName(), object->GetName()}));
 			excluds.emplace_back(id);
 			Dictionary::Ptr data = new Dictionary({{"environment_id", m_EnvironmentId}, {"timeperiod_id", objectKey}, {"exclude_id", excludeId}});
 			excluds.emplace_back(JsonEncode(data));
@@ -822,40 +859,9 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		return;
 	}
 
-	if (type == Zone::TypeInstance) {
-		Zone::Ptr zone = static_pointer_cast<Zone>(object);
-
-		Array::Ptr parents(new Array);
-		auto parentsRaw (zone->GetAllParentsRaw());
-
-		parents->Reserve(parentsRaw.size());
-
-		auto& parnts (hMSets[m_PrefixConfigObject + typeName + ":parent"]);
-
-		for (auto& parent : parentsRaw) {
-			String id = HashValue(new Array(Prepend(env, Prepend(GetObjectIdentifiersWithoutEnv(parent), GetObjectIdentifiersWithoutEnv(object)))));
-			parnts.emplace_back(id);
-			Dictionary::Ptr data = new Dictionary({{"zone_id", objectKey}, {"environment_id", m_EnvironmentId}, {"parent_id", GetObjectIdentifier(parent)}});
-			parnts.emplace_back(JsonEncode(data));
-
-			if (runtimeUpdate) {
-				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":parent", data);
-			}
-
-			parents->Add(GetObjectIdentifier(parent));
-		}
-
-		return;
-	}
-
 	if (type == User::TypeInstance) {
 		User::Ptr user = static_pointer_cast<User>(object);
-
-		Array::Ptr groups;
-		ConfigObject::Ptr (*getGroup)(const String& name);
-
-		groups = user->GetGroups();
-		getGroup = &::GetObjectByName<UserGroup>;
+		Array::Ptr groups = user->GetGroups();
 
 		if (groups) {
 			ObjectLock groupsLock(groups);
@@ -864,17 +870,28 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 			groupIds->Reserve(groups->GetLength());
 
 			auto& members (hMSets[m_PrefixConfigObject + typeName + "group:member"]);
+			auto& notificationRecipients (hMSets[m_PrefixConfigObject + "notification:recipient"]);
 
 			for (auto& group : groups) {
-				auto groupObj ((*getGroup)(group));
+				UserGroup::Ptr groupObj = UserGroup::GetByName(group);
 				String groupId = GetObjectIdentifier(groupObj);
-				String id = HashValue(new Array(Prepend(env, Prepend(GetObjectIdentifiersWithoutEnv(groupObj), GetObjectIdentifiersWithoutEnv(object)))));
+				String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()}));
 				members.emplace_back(id);
 				Dictionary::Ptr data = new Dictionary({{"user_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", groupId}});
 				members.emplace_back(JsonEncode(data));
 
 				if (runtimeUpdate) {
 					AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + "group:member", data);
+
+					// Recipients are handled by notifications during initial dumps and only need to be handled here during runtime (e.g. User creation).
+					for (auto& notification : groupObj->GetNotifications()) {
+						String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), groupObj->GetName(), notification->GetName()}));
+						notificationRecipients.emplace_back(recipientId);
+						Dictionary::Ptr recipientData = new Dictionary({{"notification_id", GetObjectIdentifier(notification)}, {"environment_id", m_EnvironmentId}, {"user_id", objectKey}, {"usergroup_id", groupId}});
+						notificationRecipients.emplace_back(JsonEncode(recipientData));
+
+						AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + "notification:recipient", recipientData);
+					}
 				}
 
 				groupIds->Add(groupId);
@@ -888,10 +905,6 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		Notification::Ptr notification = static_pointer_cast<Notification>(object);
 
 		std::set<User::Ptr> users = notification->GetUsers();
-
-		std::set<User::Ptr> allUsers;
-		std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin()));
-
 		Array::Ptr userIds = new Array();
 
 		auto usergroups(notification->GetUserGroups());
@@ -900,16 +913,22 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		userIds->Reserve(users.size());
 
 		auto& usrs (hMSets[m_PrefixConfigObject + typeName + ":user"]);
+		auto& notificationRecipients (hMSets[m_PrefixConfigObject + typeName + ":recipient"]);
 
 		for (auto& user : users) {
 			String userId = GetObjectIdentifier(user);
-			String id = HashValue(new Array(Prepend(env, Prepend(GetObjectIdentifiersWithoutEnv(user), GetObjectIdentifiersWithoutEnv(object)))));
+			String id = HashValue(new Array({m_EnvironmentId, "user", user->GetName(), object->GetName()}));
 			usrs.emplace_back(id);
+			notificationRecipients.emplace_back(id);
+
 			Dictionary::Ptr data = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}});
-			usrs.emplace_back(JsonEncode(data));
+			String dataJson = JsonEncode(data);
+			usrs.emplace_back(dataJson);
+			notificationRecipients.emplace_back(dataJson);
 
 			if (runtimeUpdate) {
 				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":user", data);
+				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", data);
 			}
 
 			userIds->Add(userId);
@@ -918,41 +937,36 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 		usergroupIds->Reserve(usergroups.size());
 
 		auto& groups (hMSets[m_PrefixConfigObject + typeName + ":usergroup"]);
-		auto& notificationRecipients (hMSets[m_PrefixConfigObject + typeName + ":recipient"]);
 
 		for (auto& usergroup : usergroups) {
 			String usergroupId = GetObjectIdentifier(usergroup);
-
-			auto groupMembers = usergroup->GetMembers();
-			std::copy(groupMembers.begin(), groupMembers.end(), std::inserter(allUsers, allUsers.begin()));
-
-			String id = HashValue(new Array(Prepend(env, Prepend("usergroup", Prepend(GetObjectIdentifiersWithoutEnv(usergroup), GetObjectIdentifiersWithoutEnv(object))))));
+			String id = HashValue(new Array({m_EnvironmentId, "usergroup", usergroup->GetName(), object->GetName()}));
 			groups.emplace_back(id);
-			Dictionary::Ptr groupData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}});
-			groups.emplace_back(JsonEncode(groupData));
-
 			notificationRecipients.emplace_back(id);
-			Dictionary::Ptr notificationRecipientData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}});
-			notificationRecipients.emplace_back(JsonEncode(notificationRecipientData));
+
+			Dictionary::Ptr groupData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}});
+			String groupDataJson = JsonEncode(groupData);
+			groups.emplace_back(groupDataJson);
+			notificationRecipients.emplace_back(groupDataJson);
 
 			if (runtimeUpdate) {
 				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":usergroup", groupData);
-				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", notificationRecipientData);
+				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", groupData);
 			}
 
-			usergroupIds->Add(usergroupId);
-		}
+			for (const User::Ptr& user : usergroup->GetMembers()) {
+				String userId = GetObjectIdentifier(user);
+				String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), usergroup->GetName(), notification->GetName()}));
+				notificationRecipients.emplace_back(recipientId);
+				Dictionary::Ptr userData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}, {"usergroup_id", usergroupId}});
+				notificationRecipients.emplace_back(JsonEncode(userData));
 
-		for (auto& user : allUsers) {
-			String userId = GetObjectIdentifier(user);
-			String id = HashValue(new Array(Prepend(env, Prepend("user", Prepend(GetObjectIdentifiersWithoutEnv(user), GetObjectIdentifiersWithoutEnv(object))))));
-			notificationRecipients.emplace_back(id);
-			Dictionary::Ptr data = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}});
-			notificationRecipients.emplace_back(JsonEncode(data));
-
-			if (runtimeUpdate) {
-				AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", data);
+				if (runtimeUpdate) {
+					AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + typeName + ":recipient", userData);
+				}
 			}
+
+			usergroupIds->Add(usergroupId);
 		}
 
 		return;
@@ -995,11 +1009,11 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 					}
 				}
 
-				values->Set("command_id", objectKey);
+				values->Set(objectKeyName, objectKey);
 				values->Set("argument_key", kv.first);
 				values->Set("environment_id", m_EnvironmentId);
 
-				String id = HashValue(new Array(Prepend(env, Prepend(kv.first, GetObjectIdentifiersWithoutEnv(object)))));
+				String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
 
 				typeArgs.emplace_back(id);
 				typeArgs.emplace_back(JsonEncode(values));
@@ -1044,11 +1058,11 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
 					}
 				}
 
-				values->Set("command_id", objectKey);
+				values->Set(objectKeyName, objectKey);
 				values->Set("envvar_key", kv.first);
 				values->Set("environment_id", m_EnvironmentId);
 
-				String id = HashValue(new Array(Prepend(env, Prepend(kv.first, GetObjectIdentifiersWithoutEnv(object)))));
+				String id = HashValue(new Array({m_EnvironmentId, kv.first, object->GetName()}));
 
 				typeVars.emplace_back(id);
 				typeVars.emplace_back(JsonEncode(values));
@@ -1105,8 +1119,7 @@ void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpd
 		m_Rcon->FireAndForgetQuery({"HSET", m_PrefixConfigCheckSum + typeName + ":state", objectKey, JsonEncode(new Dictionary({{"checksum", checksum}}))}, Prio::RuntimeStateSync);
 
 		if (runtimeUpdate) {
-			state->Set("checksum", checksum);
-			AddObjectDataToRuntimeUpdates(runtimeUpdates, objectKey, m_PrefixConfigObject + typeName + ":state", state);
+			SendStatusUpdate(checkable);
 		}
 	}
 
@@ -1147,10 +1160,11 @@ void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpd
 void IcingaDB::AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
 		const String& redisKey, const Dictionary::Ptr& data)
 {
-	data->Set("id", objectKey);
-	data->Set("redis_key", redisKey);
-	data->Set("runtime_type", "upsert");
-	runtimeUpdates.emplace_back(data);
+	Dictionary::Ptr dataClone = data->ShallowClone();
+	dataClone->Set("id", objectKey);
+	dataClone->Set("redis_key", redisKey);
+	dataClone->Set("runtime_type", "upsert");
+	runtimeUpdates.emplace_back(dataClone);
 }
 
 // Takes object and collects IcingaDB relevant attributes and computes checksums. Returns whether the object is relevant
@@ -1186,7 +1200,7 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
 
 		Zone::Ptr parent = zone->GetParent();
 		if (parent) {
-			attributes->Set("parent_id", GetObjectIdentifier(zone));
+			attributes->Set("parent_id", GetObjectIdentifier(parent));
 		}
 
 		auto parentsRaw (zone->GetAllParentsRaw());
@@ -1239,11 +1253,11 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
 		String notesUrl = checkable->GetNotesUrl();
 		String iconImage = checkable->GetIconImage();
 		if (!actionUrl.IsEmpty())
-			attributes->Set("action_url_id", HashValue(new Array({GetEnvironment(), actionUrl})));
+			attributes->Set("action_url_id", HashValue(new Array({m_EnvironmentId, actionUrl})));
 		if (!notesUrl.IsEmpty())
-			attributes->Set("notes_url_id", HashValue(new Array({GetEnvironment(), notesUrl})));
+			attributes->Set("notes_url_id", HashValue(new Array({m_EnvironmentId, notesUrl})));
 		if (!iconImage.IsEmpty())
-			attributes->Set("icon_image_id", HashValue(new Array({GetEnvironment(), iconImage})));
+			attributes->Set("icon_image_id", HashValue(new Array({m_EnvironmentId, iconImage})));
 
 
 		Host::Ptr host;
@@ -1297,7 +1311,7 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
 
 		tie(host, service) = GetHostService(notification->GetCheckable());
 
-		attributes->Set("command_id", GetObjectIdentifier(notification->GetCommand()));
+		attributes->Set("notificationcommand_id", GetObjectIdentifier(notification->GetCommand()));
 
 		attributes->Set("host_id", GetObjectIdentifier(host));
 		if (service)
@@ -1357,6 +1371,7 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
 		attributes->Set("entry_time", TimestampToMilliseconds(downtime->GetEntryTime()));
 		attributes->Set("scheduled_start_time", TimestampToMilliseconds(downtime->GetStartTime()));
 		attributes->Set("scheduled_end_time", TimestampToMilliseconds(downtime->GetEndTime()));
+		attributes->Set("scheduled_duration", TimestampToMilliseconds(downtime->GetEndTime() - downtime->GetStartTime()));
 		attributes->Set("flexible_duration", TimestampToMilliseconds(downtime->GetDuration()));
 		attributes->Set("is_flexible", !downtime->GetFixed());
 		attributes->Set("is_in_effect", downtime->IsInEffect());
@@ -1365,6 +1380,12 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
 			attributes->Set("end_time", TimestampToMilliseconds(downtime->GetFixed() ? downtime->GetEndTime() : (downtime->GetTriggerTime() + downtime->GetDuration())));
 		}
 
+		auto duration = downtime->GetDuration();
+		if (downtime->GetFixed()) {
+			duration = downtime->GetEndTime() - downtime->GetStartTime();
+		}
+		attributes->Set("duration", TimestampToMilliseconds(duration));
+
 		Host::Ptr host;
 		Service::Ptr service;
 		tie(host, service) = GetHostService(downtime->GetCheckable());
@@ -1475,7 +1496,8 @@ IcingaDB::CreateConfigUpdate(const ConfigObject::Ptr& object, const String typeN
 
 void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object)
 {
-	String typeName = object->GetReflectionType()->GetName().ToLower();
+	Type::Ptr type = object->GetReflectionType();
+	String typeName = type->GetName().ToLower();
 	String objectKey = GetObjectIdentifier(object);
 
 	m_Rcon->FireAndForgetQueries({
@@ -1487,12 +1509,23 @@ void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object)
 		}
    	}, Prio::Config);
 
-	auto checkable (dynamic_pointer_cast<Checkable>(object));
+	CustomVarObject::Ptr customVarObject = dynamic_pointer_cast<CustomVarObject>(object);
+
+	if (customVarObject) {
+		Dictionary::Ptr vars = customVarObject->GetVars();
+		SendCustomVarsChanged(object, vars, nullptr);
+	}
+
+	if (type == Host::TypeInstance || type == Service::TypeInstance) {
+		Checkable::Ptr checkable = static_pointer_cast<Checkable>(object);
+
+		Host::Ptr host;
+		Service::Ptr service;
+		tie(host, service) = GetHostService(checkable);
 
-	if (checkable) {
 		m_Rcon->FireAndForgetQuery({
 			"ZREM",
-			dynamic_pointer_cast<Service>(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
+			service ? "icinga:nextupdate:service" : "icinga:nextupdate:host",
 			GetObjectIdentifier(checkable)
 		}, Prio::CheckResult);
 
@@ -1500,6 +1533,42 @@ void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object)
 			{"HDEL", m_PrefixConfigObject + typeName + ":state", objectKey},
 			{"HDEL", m_PrefixConfigCheckSum + typeName + ":state", objectKey}
 		}, Prio::RuntimeStateSync);
+
+		if (service) {
+			SendGroupsChanged<ServiceGroup>(checkable, service->GetGroups(), nullptr);
+		} else {
+			SendGroupsChanged<HostGroup>(checkable, host->GetGroups(), nullptr);
+		}
+
+		return;
+	}
+
+	if (type == TimePeriod::TypeInstance) {
+		TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
+		SendTimePeriodRangesChanged(timeperiod, timeperiod->GetRanges(), nullptr);
+		SendTimePeriodIncludesChanged(timeperiod, timeperiod->GetIncludes(), nullptr);
+		SendTimePeriodExcludesChanged(timeperiod, timeperiod->GetExcludes(), nullptr);
+		return;
+	}
+
+	if (type == User::TypeInstance) {
+		User::Ptr user = static_pointer_cast<User>(object);
+		SendGroupsChanged<UserGroup>(user, user->GetGroups(), nullptr);
+		return;
+	}
+
+	if (type == Notification::TypeInstance) {
+		Notification::Ptr notification = static_pointer_cast<Notification>(object);
+		SendNotificationUsersChanged(notification, notification->GetUsersRaw(), nullptr);
+		SendNotificationUserGroupsChanged(notification, notification->GetUserGroupsRaw(), nullptr);
+		return;
+	}
+
+	if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) {
+		Command::Ptr command = static_pointer_cast<Command>(object);
+		SendCommandArgumentsChanged(command, command->GetArguments(), nullptr);
+		SendCommandEnvChanged(command, command->GetEnv(), nullptr);
+		return;
 	}
 }
 
@@ -1549,6 +1618,9 @@ void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResul
 	if (!checkable)
 		return;
 
+	if (!cr)
+		return;
+
 	Host::Ptr host;
 	Service::Ptr service;
 
@@ -1563,9 +1635,15 @@ void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResul
 		hard_state = service ? Convert::ToLong(service->GetLastHardState()) : Convert::ToLong(host->GetLastHardState());
 	}
 
+	auto eventTime (cr->GetExecutionEnd());
+	auto eventTs (TimestampToMilliseconds(eventTime));
+
+	Array::Ptr rawId = new Array({m_EnvironmentId, object->GetName()});
+	rawId->Add(eventTs);
+
 	std::vector<String> xAdd ({
 		"XADD", "icinga:history:stream:state", "*",
-		"id", Utility::NewUniqueID(),
+		"id", HashValue(rawId),
 		"environment_id", m_EnvironmentId,
 		"host_id", GetObjectIdentifier(host),
 		"state_type", Convert::ToString(type),
@@ -1575,8 +1653,8 @@ void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResul
 		"previous_soft_state", Convert::ToString(GetPreviousState(checkable, service, StateTypeSoft)),
 		"previous_hard_state", Convert::ToString(GetPreviousState(checkable, service, StateTypeHard)),
 		"max_check_attempts", Convert::ToString(checkable->GetMaxCheckAttempts()),
-		"event_time", Convert::ToString(TimestampToMilliseconds(cr ? cr->GetExecutionEnd() : Utility::GetTime())),
-		"event_id", Utility::NewUniqueID(),
+		"event_time", Convert::ToString(eventTs),
+		"event_id", CalcEventID("state_change", object, eventTime),
 		"event_type", "state_change"
 	});
 
@@ -1596,6 +1674,8 @@ void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResul
 		xAdd.emplace_back(Utility::ValidateUTF8(std::move(output)));
 		xAdd.emplace_back("check_source");
 		xAdd.emplace_back(cr->GetCheckSource());
+		xAdd.emplace_back("scheduling_source");
+		xAdd.emplace_back(cr->GetSchedulingSource());
 	}
 
 	if (service) {
@@ -1620,7 +1700,7 @@ void IcingaDB::SendStateChange(const ConfigObject::Ptr& object, const CheckResul
 
 void IcingaDB::SendSentNotification(
 	const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
-	NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text
+	NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text, double sendTime
 )
 {
 	if (!m_Rcon || !m_Rcon->IsConnected())
@@ -1636,7 +1716,13 @@ void IcingaDB::SendSentNotification(
 	}
 
 	auto usersAmount (users.size());
-	auto notificationHistoryId = Utility::NewUniqueID();
+	auto sendTs (TimestampToMilliseconds(sendTime));
+
+	Array::Ptr rawId = new Array({m_EnvironmentId, notification->GetName()});
+	rawId->Add(GetNotificationTypeByEnum(type));
+	rawId->Add(sendTs);
+
+	auto notificationHistoryId (HashValue(rawId));
 
 	std::vector<String> xAdd ({
 		"XADD", "icinga:history:stream:notification", "*",
@@ -1650,8 +1736,8 @@ void IcingaDB::SendSentNotification(
 		"author", Utility::ValidateUTF8(author),
 		"text", Utility::ValidateUTF8(finalText),
 		"users_notified", Convert::ToString(usersAmount),
-		"send_time", Convert::ToString(TimestampToMilliseconds(Utility::GetTime())),
-		"event_id", Utility::NewUniqueID(),
+		"send_time", Convert::ToString(sendTs),
+		"event_id", CalcEventID("notification", notification, sendTime, type),
 		"event_type", "notification"
 	});
 
@@ -1672,20 +1758,16 @@ void IcingaDB::SendSentNotification(
 		xAdd.emplace_back(GetObjectIdentifier(endpoint));
 	}
 
-	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
-
-	for (const User::Ptr& user : users) {
-		auto userId = GetObjectIdentifier(user);
-		std::vector<String> xAddUser ({
-			"XADD", "icinga:history:stream:usernotification", "*",
-			"id", Utility::NewUniqueID(),
-			"environment_id", m_EnvironmentId,
-			"notification_history_id", notificationHistoryId,
-			"user_id", GetObjectIdentifier(user),
-		});
-
-		m_Rcon->FireAndForgetQuery(std::move(xAddUser), Prio::History);
+	if (!users.empty()) {
+		Array::Ptr users_notified = new Array();
+		for (const User::Ptr& user : users) {
+			users_notified->Add(GetObjectIdentifier(user));
+		}
+		xAdd.emplace_back("users_notified_ids");
+		xAdd.emplace_back(JsonEncode(users_notified));
 	}
+
+	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
 }
 
 void IcingaDB::SendStartedDowntime(const Downtime::Ptr& downtime)
@@ -1711,12 +1793,12 @@ void IcingaDB::SendStartedDowntime(const Downtime::Ptr& downtime)
 		"author", Utility::ValidateUTF8(downtime->GetAuthor()),
 		"comment", Utility::ValidateUTF8(downtime->GetComment()),
 		"is_flexible", Convert::ToString((unsigned short)!downtime->GetFixed()),
-		"flexible_duration", Convert::ToString(downtime->GetDuration()),
+		"flexible_duration", Convert::ToString(TimestampToMilliseconds(downtime->GetDuration())),
 		"scheduled_start_time", Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())),
 		"scheduled_end_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())),
 		"has_been_cancelled", Convert::ToString((unsigned short)downtime->GetWasCancelled()),
 		"trigger_time", Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())),
-		"event_id", Utility::NewUniqueID(),
+		"event_id", CalcEventID("downtime_start", downtime),
 		"event_type", "downtime_start"
 	});
 
@@ -1761,6 +1843,13 @@ void IcingaDB::SendStartedDowntime(const Downtime::Ptr& downtime)
 		xAdd.emplace_back(GetObjectIdentifier(parent));
 	}
 
+	auto scheduledBy (downtime->GetScheduledBy());
+
+	if (!scheduledBy.IsEmpty()) {
+		xAdd.emplace_back("scheduled_by");
+		xAdd.emplace_back(scheduledBy);
+	}
+
 	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
 }
 
@@ -1790,13 +1879,13 @@ void IcingaDB::SendRemovedDowntime(const Downtime::Ptr& downtime)
 		"cancelled_by", Utility::ValidateUTF8(downtime->GetRemovedBy()),
 		"comment", Utility::ValidateUTF8(downtime->GetComment()),
 		"is_flexible", Convert::ToString((unsigned short)!downtime->GetFixed()),
-		"flexible_duration", Convert::ToString(downtime->GetDuration()),
+		"flexible_duration", Convert::ToString(TimestampToMilliseconds(downtime->GetDuration())),
 		"scheduled_start_time", Convert::ToString(TimestampToMilliseconds(downtime->GetStartTime())),
 		"scheduled_end_time", Convert::ToString(TimestampToMilliseconds(downtime->GetEndTime())),
 		"has_been_cancelled", Convert::ToString((unsigned short)downtime->GetWasCancelled()),
 		"trigger_time", Convert::ToString(TimestampToMilliseconds(downtime->GetTriggerTime())),
 		"cancel_time", Convert::ToString(TimestampToMilliseconds(Utility::GetTime())),
-		"event_id", Utility::NewUniqueID(),
+		"event_id", CalcEventID("downtime_end", downtime),
 		"event_type", "downtime_end"
 	});
 
@@ -1841,6 +1930,13 @@ void IcingaDB::SendRemovedDowntime(const Downtime::Ptr& downtime)
 		xAdd.emplace_back(GetObjectIdentifier(parent));
 	}
 
+	auto scheduledBy (downtime->GetScheduledBy());
+
+	if (!scheduledBy.IsEmpty()) {
+		xAdd.emplace_back("scheduled_by");
+		xAdd.emplace_back(scheduledBy);
+	}
+
 	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
 }
 
@@ -1866,7 +1962,7 @@ void IcingaDB::SendAddedComment(const Comment::Ptr& comment)
 		"entry_type", Convert::ToString(comment->GetEntryType()),
 		"is_persistent", Convert::ToString((unsigned short)comment->GetPersistent()),
 		"is_sticky", Convert::ToString((unsigned short)(comment->GetEntryType() == CommentAcknowledgement && comment->GetCheckable()->GetAcknowledgement() == AcknowledgementSticky)),
-		"event_id", Utility::NewUniqueID(),
+		"event_id", CalcEventID("comment_add", comment),
 		"event_type", "comment_add"
 	});
 
@@ -1923,7 +2019,7 @@ void IcingaDB::SendRemovedComment(const Comment::Ptr& comment)
 		"entry_type", Convert::ToString(comment->GetEntryType()),
 		"is_persistent", Convert::ToString((unsigned short)comment->GetPersistent()),
 		"is_sticky", Convert::ToString((unsigned short)(comment->GetEntryType() == CommentAcknowledgement && comment->GetCheckable()->GetAcknowledgement() == AcknowledgementSticky)),
-		"event_id", Utility::NewUniqueID(),
+		"event_id", CalcEventID("comment_remove", comment),
 		"event_type", "comment_remove"
 	});
 
@@ -1984,8 +2080,7 @@ void IcingaDB::SendFlappingChange(const Checkable::Ptr& checkable, double change
 		"environment_id", m_EnvironmentId,
 		"host_id", GetObjectIdentifier(host),
 		"flapping_threshold_low", Convert::ToString(checkable->GetFlappingThresholdLow()),
-		"flapping_threshold_high", Convert::ToString(checkable->GetFlappingThresholdHigh()),
-		"event_id", Utility::NewUniqueID()
+		"flapping_threshold_high", Convert::ToString(checkable->GetFlappingThresholdHigh())
 	});
 
 	if (service) {
@@ -2027,8 +2122,10 @@ void IcingaDB::SendFlappingChange(const Checkable::Ptr& checkable, double change
 
 	xAdd.emplace_back("start_time");
 	xAdd.emplace_back(Convert::ToString(startTime));
+	xAdd.emplace_back("event_id");
+	xAdd.emplace_back(CalcEventID(checkable->IsFlapping() ? "flapping_start" : "flapping_end", checkable, startTime));
 	xAdd.emplace_back("id");
-	xAdd.emplace_back(HashValue(new Array({GetEnvironment(), checkable->GetReflectionType()->GetName(), checkable->GetName(), startTime})));
+	xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), startTime})));
 
 	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
 }
@@ -2071,7 +2168,6 @@ void IcingaDB::SendAcknowledgementSet(const Checkable::Ptr& checkable, const Str
 
 	std::vector<String> xAdd ({
 		"XADD", "icinga:history:stream:acknowledgement", "*",
-		"event_id", Utility::NewUniqueID(),
 		"environment_id", m_EnvironmentId,
 		"host_id", GetObjectIdentifier(host),
 		"event_type", "ack_set",
@@ -2107,8 +2203,10 @@ void IcingaDB::SendAcknowledgementSet(const Checkable::Ptr& checkable, const Str
 
 	xAdd.emplace_back("set_time");
 	xAdd.emplace_back(Convert::ToString(setTime));
+	xAdd.emplace_back("event_id");
+	xAdd.emplace_back(CalcEventID("ack_set", checkable, setTime));
 	xAdd.emplace_back("id");
-	xAdd.emplace_back(HashValue(new Array({GetEnvironment(), checkable->GetReflectionType()->GetName(), checkable->GetName(), setTime})));
+	xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), setTime})));
 
 	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
 }
@@ -2124,7 +2222,6 @@ void IcingaDB::SendAcknowledgementCleared(const Checkable::Ptr& checkable, const
 
 	std::vector<String> xAdd ({
 		"XADD", "icinga:history:stream:acknowledgement", "*",
-		"event_id", Utility::NewUniqueID(),
 		"environment_id", m_EnvironmentId,
 		"host_id", GetObjectIdentifier(host),
 		"clear_time", Convert::ToString(TimestampToMilliseconds(changeTime)),
@@ -2152,8 +2249,10 @@ void IcingaDB::SendAcknowledgementCleared(const Checkable::Ptr& checkable, const
 
 	xAdd.emplace_back("set_time");
 	xAdd.emplace_back(Convert::ToString(setTime));
+	xAdd.emplace_back("event_id");
+	xAdd.emplace_back(CalcEventID("ack_clear", checkable, setTime));
 	xAdd.emplace_back("id");
-	xAdd.emplace_back(HashValue(new Array({GetEnvironment(), checkable->GetReflectionType()->GetName(), checkable->GetName(), setTime})));
+	xAdd.emplace_back(HashValue(new Array({m_EnvironmentId, checkable->GetName(), setTime})));
 
 	if (!removedBy.IsEmpty()) {
 		xAdd.emplace_back("cleared_by");
@@ -2163,6 +2262,150 @@ void IcingaDB::SendAcknowledgementCleared(const Checkable::Ptr& checkable, const
 	m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History);
 }
 
+void IcingaDB::SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<Value> deletedUsers = GetArrayDeletedValues(oldValues, newValues);
+
+	for (const auto& userName : deletedUsers) {
+		String id = HashValue(new Array({m_EnvironmentId, "user", userName, notification->GetName()}));
+		DeleteRelationship(id, "notification:user");
+		DeleteRelationship(id, "notification:recipient");
+	}
+}
+
+void IcingaDB::SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<Value> deletedUserGroups = GetArrayDeletedValues(oldValues, newValues);
+
+	for (const auto& userGroupName : deletedUserGroups) {
+		UserGroup::Ptr userGroup = UserGroup::GetByName(userGroupName);
+		String id = HashValue(new Array({m_EnvironmentId, "usergroup", userGroupName, notification->GetName()}));
+		DeleteRelationship(id, "notification:usergroup");
+		DeleteRelationship(id, "notification:recipient");
+
+		for (const User::Ptr& user : userGroup->GetMembers()) {
+			String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), userGroupName, notification->GetName()}));
+			DeleteRelationship(userId, "notification:recipient");
+		}
+	}
+}
+
+void IcingaDB::SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+	String typeName = GetLowerCaseTypeNameDB(timeperiod);
+
+	for (const auto& rangeKey : deletedKeys) {
+		String id = HashValue(new Array({m_EnvironmentId, rangeKey, oldValues->Get(rangeKey), timeperiod->GetName()}));
+		DeleteRelationship(id, "timeperiod:range");
+	}
+}
+
+void IcingaDB::SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<Value> deletedIncludes = GetArrayDeletedValues(oldValues, newValues);
+
+	for (const auto& includeName : deletedIncludes) {
+		String id = HashValue(new Array({m_EnvironmentId, includeName, timeperiod->GetName()}));
+		DeleteRelationship(id, "timeperiod:override:include");
+	}
+}
+
+void IcingaDB::SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<Value> deletedExcludes = GetArrayDeletedValues(oldValues, newValues);
+
+	for (const auto& excludeName : deletedExcludes) {
+		String id = HashValue(new Array({m_EnvironmentId, excludeName, timeperiod->GetName()}));
+		DeleteRelationship(id, "timeperiod:override:exclude");
+	}
+}
+
+template<typename T>
+void IcingaDB::SendGroupsChanged(const ConfigObject::Ptr& object, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<Value> deletedGroups = GetArrayDeletedValues(oldValues, newValues);
+	String typeName = GetLowerCaseTypeNameDB(object);
+
+	for (const auto& groupName : deletedGroups) {
+		typename T::Ptr group = ConfigObject::GetObject<T>(groupName);
+		String id = HashValue(new Array({m_EnvironmentId, group->GetName(), object->GetName()}));
+		DeleteRelationship(id, typeName + "group:member");
+
+		if (std::is_same<T, UserGroup>::value) {
+			UserGroup::Ptr userGroup = dynamic_pointer_cast<UserGroup>(group);
+
+			for (const auto& notification : userGroup->GetNotifications()) {
+				String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", object->GetName(), groupName, notification->GetName()}));
+				DeleteRelationship(userId, "notification:recipient");
+			}
+		}
+	}
+}
+
+void IcingaDB::SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+	String typeName = GetLowerCaseTypeNameDB(command);
+
+	for (const auto& envvarKey : deletedKeys) {
+		String id = HashValue(new Array({m_EnvironmentId, envvarKey, command->GetName()}));
+		DeleteRelationship(id, typeName + ":envvar", true);
+	}
+}
+
+void IcingaDB::SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	std::vector<String> deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues);
+	String typeName = GetLowerCaseTypeNameDB(command);
+
+	for (const auto& argumentKey : deletedKeys) {
+		String id = HashValue(new Array({m_EnvironmentId, argumentKey, command->GetName()}));
+		DeleteRelationship(id, typeName + ":argument", true);
+	}
+}
+
+void IcingaDB::SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) {
+		return;
+	}
+
+	Dictionary::Ptr oldVars = SerializeVars(oldValues);
+	Dictionary::Ptr newVars = SerializeVars(newValues);
+
+	std::vector<String> deletedVars = GetDictionaryDeletedKeys(oldVars, newVars);
+	String typeName = GetLowerCaseTypeNameDB(object);
+
+	for (const auto& varId : deletedVars) {
+		String id = HashValue(new Array({m_EnvironmentId, varId, object->GetName()}));
+		DeleteRelationship(id, typeName + ":customvar");
+	}
+}
+
 Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
 {
 	Dictionary::Ptr attrs = new Dictionary();
@@ -2192,6 +2435,7 @@ Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
 		attrs->Set("state", state);
 		attrs->Set("hard_state", service->HasBeenChecked() ? service->GetLastHardState() : 99);
 		attrs->Set("severity", service->GetSeverity());
+		attrs->Set("host_id", GetObjectIdentifier(host));
 	} else {
 		attrs->Set("host_id", id);
 		auto state = host->HasBeenChecked() ? host->GetState() : 99;
@@ -2235,6 +2479,7 @@ Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
 		attrs->Set("execution_time", TimestampToMilliseconds(fmax(0.0, cr->CalculateExecutionTime())));
 		attrs->Set("latency", TimestampToMilliseconds(cr->CalculateLatency()));
 		attrs->Set("check_source", cr->GetCheckSource());
+		attrs->Set("scheduling_source", cr->GetSchedulingSource());
 	}
 
 	attrs->Set("is_problem", checkable->GetProblem());
@@ -2388,10 +2633,11 @@ void IcingaDB::NotificationSentToAllUsersHandler(
 )
 {
 	auto rws (ConfigType::GetObjectsByType<IcingaDB>());
+	auto sendTime (notification->GetLastNotification());
 
 	if (!rws.empty()) {
 		for (auto& rw : rws) {
-			rw->SendSentNotification(notification, checkable, users, type, cr, author, text);
+			rw->SendSentNotification(notification, checkable, users, type, cr, author, text, sendTime);
 		}
 	}
 }
@@ -2459,3 +2705,89 @@ void IcingaDB::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, co
 		}
 	}
 }
+
+void IcingaDB::NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendNotificationUsersChanged(notification, oldValues, newValues);
+	}
+}
+
+void IcingaDB::NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendNotificationUserGroupsChanged(notification, oldValues, newValues);
+	}
+}
+
+void IcingaDB::TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendTimePeriodRangesChanged(timeperiod, oldValues, newValues);
+	}
+}
+
+void IcingaDB::TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendTimePeriodIncludesChanged(timeperiod, oldValues, newValues);
+	}
+}
+
+void IcingaDB::TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendTimePeriodExcludesChanged(timeperiod, oldValues, newValues);
+	}
+}
+
+void IcingaDB::UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendGroupsChanged<UserGroup>(user, oldValues, newValues);
+	}
+}
+
+void IcingaDB::HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendGroupsChanged<HostGroup>(host, oldValues, newValues);
+	}
+}
+
+void IcingaDB::ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendGroupsChanged<ServiceGroup>(service, oldValues, newValues);
+	}
+}
+
+void IcingaDB::CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendCommandEnvChanged(command, oldValues, newValues);
+	}
+}
+
+void IcingaDB::CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendCommandArgumentsChanged(command, oldValues, newValues);
+	}
+}
+
+void IcingaDB::CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) {
+	for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
+		rw->SendCustomVarsChanged(object, oldValues, newValues);
+	}
+}
+
+void IcingaDB::DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum) {
+	Log(LogNotice, "IcingaDB") << "Deleting relationship '" << redisKeyWithoutPrefix << " -> '" << id << "'";
+
+	String redisKey = m_PrefixConfigObject + redisKeyWithoutPrefix;
+
+	std::vector<std::vector<String>> queries;
+
+	if (hasChecksum) {
+		queries.push_back({"HDEL", m_PrefixConfigCheckSum + redisKeyWithoutPrefix, id});
+	}
+
+	queries.push_back({"HDEL", redisKey, id});
+	queries.push_back({
+		"XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*",
+		"redis_key", redisKey, "id", id, "runtime_type", "delete"
+	});
+
+	m_Rcon->FireAndForgetQueries(queries, Prio::Config);
+}


=====================================
lib/icingadb/icingadb-stats.cpp
=====================================
@@ -1,6 +1,7 @@
 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
 
 #include "icingadb/icingadb.hpp"
+#include "base/application.hpp"
 #include "base/json.hpp"
 #include "base/logger.hpp"
 #include "base/serializer.hpp"
@@ -38,13 +39,14 @@ Dictionary::Ptr IcingaDB::GetStats()
 		}));
 	}
 
-	auto localEndpoint (Endpoint::GetLocalEndpoint());
+	typedef Dictionary::Ptr DP;
+	DP app = DP(DP(DP(stats->Get("IcingaApplication"))->Get("status"))->Get("icingaapplication"))->Get("app");
 
-	if (localEndpoint) {
-		typedef Dictionary::Ptr DP;
+	app->Set("program_start", TimestampToMilliseconds(Application::GetStartTime()));
 
-		DP(DP(DP(DP(stats->Get("IcingaApplication"))->Get("status"))->Get("icingaapplication"))->Get("app"))
-			->Set("endpoint_id", GetObjectIdentifier(localEndpoint));
+	auto localEndpoint (Endpoint::GetLocalEndpoint());
+	if (localEndpoint) {
+		app->Set("endpoint_id", GetObjectIdentifier(localEndpoint));
 	}
 
 	return stats;


=====================================
lib/icingadb/icingadb-utility.cpp
=====================================
@@ -60,30 +60,37 @@ String IcingaDB::FormatCommandLine(const Value& commandLine)
 	return result;
 }
 
-String IcingaDB::GetEnvironment()
+String IcingaDB::GetObjectIdentifier(const ConfigObject::Ptr& object)
 {
-	return ConfigType::GetObjectsByType<IcingaApplication>()[0]->GetEnvironment();
+	return HashValue(new Array({m_EnvironmentId, object->GetName()}));
 }
 
-ArrayData IcingaDB::GetObjectIdentifiersWithoutEnv(const ConfigObject::Ptr& object)
+/**
+ * Calculates a deterministic history event ID like SHA1(env, eventType, x...[, nt][, eventTime])
+ *
+ * Where SHA1(env, x...) = GetObjectIdentifier(object)
+ */
+String IcingaDB::CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime, NotificationType nt)
 {
-	Type::Ptr type = object->GetReflectionType();
+	Array::Ptr rawId = new Array({object->GetName()});
+	rawId->Insert(0, m_EnvironmentId);
+	rawId->Insert(1, eventType);
 
-	if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance)
-		return {type->GetName(), object->GetName()};
-	else
-		return {object->GetName()};
-}
+	if (nt) {
+		rawId->Add(GetNotificationTypeByEnum(nt));
+	}
 
-String IcingaDB::GetObjectIdentifier(const ConfigObject::Ptr& object)
-{
-	return HashValue(new Array(Prepend(GetEnvironment(), GetObjectIdentifiersWithoutEnv(object))));
+	if (eventTime) {
+		rawId->Add(TimestampToMilliseconds(eventTime));
+	}
+
+	return HashValue(std::move(rawId));
 }
 
 static const std::set<String> metadataWhitelist ({"package", "source_location", "templates"});
 
 /**
- * Prepare object's custom vars for being written to Redis
+ * Prepare custom vars for being written to Redis
  *
  * object.vars = {
  *   "disks": {
@@ -96,7 +103,7 @@ static const std::set<String> metadataWhitelist ({"package", "source_location",
  *
  * return {
  *   SHA1(PackObject([
- *     Environment,
+ *     EnvironmentId,
  *     "disks",
  *     {
  *       "disk": {},
@@ -105,7 +112,7 @@ static const std::set<String> metadataWhitelist ({"package", "source_location",
  *       }
  *     }
  *   ])): {
- *     "envId": SHA1(Environment),
+ *     "environment_id": EnvironmentId,
  *     "name_checksum": SHA1("disks"),
  *     "name": "disks",
  *     "value": {
@@ -117,28 +124,24 @@ static const std::set<String> metadataWhitelist ({"package", "source_location",
  *   }
  * }
  *
- * @param	object	Config object with custom vars
+ * @param	Dictionary	Config object with custom vars
  *
- * @return 			JSON-like data structure for Redis
+ * @return 				JSON-like data structure for Redis
  */
-Dictionary::Ptr IcingaDB::SerializeVars(const CustomVarObject::Ptr& object)
+Dictionary::Ptr IcingaDB::SerializeVars(const Dictionary::Ptr& vars)
 {
-	Dictionary::Ptr vars = object->GetVars();
-
 	if (!vars)
 		return nullptr;
 
 	Dictionary::Ptr res = new Dictionary();
-	auto env (GetEnvironment());
-	auto envChecksum (SHA1(env));
 
 	ObjectLock olock(vars);
 
 	for (auto& kv : vars) {
 		res->Set(
-			SHA1(PackObject((Array::Ptr)new Array({env, kv.first, kv.second}))),
+			SHA1(PackObject((Array::Ptr)new Array({m_EnvironmentId, kv.first, kv.second}))),
 			(Dictionary::Ptr)new Dictionary({
-				{"environment_id", envChecksum},
+				{"environment_id", m_EnvironmentId},
 				{"name_checksum", SHA1(kv.first)},
 				{"name", kv.first},
 				{"value", JsonEncode(kv.second)},
@@ -149,6 +152,32 @@ Dictionary::Ptr IcingaDB::SerializeVars(const CustomVarObject::Ptr& object)
 	return res;
 }
 
+const char* IcingaDB::GetNotificationTypeByEnum(NotificationType type)
+{
+	switch (type) {
+		case NotificationDowntimeStart:
+			return "downtime_start";
+		case NotificationDowntimeEnd:
+			return "downtime_end";
+		case NotificationDowntimeRemoved:
+			return "downtime_removed";
+		case NotificationCustom:
+			return "custom";
+		case NotificationAcknowledgement:
+			return "acknowledgement";
+		case NotificationProblem:
+			return "problem";
+		case NotificationRecovery:
+			return "recovery";
+		case NotificationFlappingStart:
+			return "flapping_start";
+		case NotificationFlappingEnd:
+			return "flapping_end";
+	}
+
+	VERIFY(!"Invalid notification type.");
+}
+
 static const std::set<String> propertiesBlacklistEmpty;
 
 String IcingaDB::HashValue(const Value& value)
@@ -227,3 +256,49 @@ String IcingaDB::IcingaToStreamValue(const Value& value)
 			return JsonEncode(value);
 	}
 }
+
+// Returns the items that exist in "arrayOld" but not in "arrayNew"
+std::vector<Value> IcingaDB::GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew) {
+	std::vector<Value> deletedValues;
+
+	if (!arrayOld) {
+		return deletedValues;
+	}
+
+	if (!arrayNew) {
+		return std::vector<Value>(arrayOld->Begin(), arrayOld->End());
+	}
+
+	std::vector<Value> vectorOld(arrayOld->Begin(), arrayOld->End());
+	std::sort(vectorOld.begin(), vectorOld.end());
+	vectorOld.erase(std::unique(vectorOld.begin(), vectorOld.end()), vectorOld.end());
+
+	std::vector<Value> vectorNew(arrayNew->Begin(), arrayNew->End());
+	std::sort(vectorNew.begin(), vectorNew.end());
+	vectorNew.erase(std::unique(vectorNew.begin(), vectorNew.end()), vectorNew.end());
+
+	std::set_difference(vectorOld.begin(), vectorOld.end(), vectorNew.begin(), vectorNew.end(), std::back_inserter(deletedValues));
+
+	return deletedValues;
+}
+
+// Returns the keys that exist in "dictOld" but not in "dictNew"
+std::vector<String> IcingaDB::GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew) {
+	std::vector<String> deletedKeys;
+
+	if (!dictOld) {
+		return deletedKeys;
+	}
+
+	std::vector<String> oldKeys = dictOld->GetKeys();
+
+	if (!dictNew) {
+		return oldKeys;
+	}
+
+	std::vector<String> newKeys = dictNew->GetKeys();
+
+	std::set_difference(oldKeys.begin(), oldKeys.end(), newKeys.begin(), newKeys.end(), std::back_inserter(deletedKeys));
+
+	return deletedKeys;
+}


=====================================
lib/icingadb/icingadb.cpp
=====================================
@@ -3,11 +3,16 @@
 #include "icingadb/icingadb.hpp"
 #include "icingadb/icingadb-ti.cpp"
 #include "icingadb/redisconnection.hpp"
+#include "remote/apilistener.hpp"
 #include "remote/eventqueue.hpp"
+#include "base/configuration.hpp"
 #include "base/json.hpp"
+#include "base/tlsutility.hpp"
+#include "base/utility.hpp"
 #include "icinga/checkable.hpp"
 #include "icinga/host.hpp"
 #include <boost/algorithm/string.hpp>
+#include <fstream>
 #include <memory>
 #include <utility>
 
@@ -18,7 +23,7 @@ using namespace icinga;
 using Prio = RedisConnection::QueryPriority;
 
 String IcingaDB::m_EnvironmentId;
-boost::once_flag IcingaDB::m_EnvironmentIdOnce = BOOST_ONCE_INIT;
+std::once_flag IcingaDB::m_EnvironmentIdOnce;
 
 REGISTER_TYPE(IcingaDB);
 
@@ -52,9 +57,39 @@ void IcingaDB::Start(bool runtimeCreated)
 {
 	ObjectImpl<IcingaDB>::Start(runtimeCreated);
 
-	boost::call_once([]() {
-		m_EnvironmentId = SHA1(GetEnvironment());
-	}, m_EnvironmentIdOnce);
+	std::call_once(m_EnvironmentIdOnce, []() {
+		String path = Configuration::DataDir + "/icingadb.env";
+
+		if (Utility::PathExists(path)) {
+			m_EnvironmentId = Utility::LoadJsonFile(path);
+
+			if (m_EnvironmentId.GetLength() != 2*SHA_DIGEST_LENGTH) {
+				throw std::runtime_error("Wrong length of stored Icinga DB environment");
+			}
+
+			for (unsigned char c : m_EnvironmentId) {
+				if (!std::isxdigit(c)) {
+					throw std::runtime_error("Stored Icinga DB environment is not a hex string");
+				}
+			}
+		} else {
+			std::shared_ptr<X509> cert = GetX509Certificate(ApiListener::GetDefaultCaPath());
+
+			unsigned int n;
+			unsigned char digest[EVP_MAX_MD_SIZE];
+			if (X509_pubkey_digest(cert.get(), EVP_sha1(), digest, &n) != 1) {
+				BOOST_THROW_EXCEPTION(openssl_error()
+					<< boost::errinfo_api_function("X509_pubkey_digest")
+					<< errinfo_openssl_error(ERR_peek_error()));
+			}
+
+			m_EnvironmentId = BinaryToHex(digest, n);
+
+			Utility::SaveJsonFile(path, 0600, m_EnvironmentId);
+		}
+
+		m_EnvironmentId = m_EnvironmentId.ToLower();
+	});
 
 	Log(LogInformation, "IcingaDB")
 		<< "'" << GetName() << "' started.";
@@ -151,6 +186,7 @@ void IcingaDB::PublishStats()
 	Dictionary::Ptr status = GetStats();
 	status->Set("config_dump_in_progress", m_ConfigDumpInProgress);
 	status->Set("timestamp", TimestampToMilliseconds(Utility::GetTime()));
+	status->Set("icingadb_environment", m_EnvironmentId);
 
 	std::vector<String> query {"XADD", "icinga:stats", "MAXLEN", "1", "*"};
 
@@ -204,6 +240,10 @@ void IcingaDB::DumpedGlobals::Reset()
 	m_Ids.clear();
 }
 
+String IcingaDB::GetEnvironmentId() const {
+	return m_EnvironmentId;
+}
+
 bool IcingaDB::DumpedGlobals::IsNew(const String& id)
 {
 	std::lock_guard<std::mutex> l (m_Mutex);


=====================================
lib/icingadb/icingadb.hpp
=====================================
@@ -12,7 +12,6 @@
 #include "icinga/service.hpp"
 #include "icinga/downtime.hpp"
 #include "remote/messageorigin.hpp"
-#include <boost/thread/once.hpp>
 #include <atomic>
 #include <memory>
 #include <mutex>
@@ -40,6 +39,8 @@ public:
 	virtual void Start(bool runtimeCreated) override;
 	virtual void Stop(bool runtimeRemoved) override;
 
+	String GetEnvironmentId() const override;
+
 protected:
 	void ValidateTlsProtocolmin(const Lazy<String>& lvalue, const ValidationUtils& utils) override;
 	void ValidateConnectTimeout(const Lazy<double>& lvalue, const ValidationUtils& utils) override;
@@ -78,10 +79,11 @@ private:
 	void SendStateChange(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type);
 	void AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
 			const String& redisKey, const Dictionary::Ptr& data);
+	void DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum = false);
 
 	void SendSentNotification(
 		const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
-		NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text
+		NotificationType type, const CheckResult::Ptr& cr, const String& author, const String& text, double sendTime
 	);
 
 	void SendStartedDowntime(const Downtime::Ptr& downtime);
@@ -92,6 +94,16 @@ private:
 	void SendNextUpdate(const Checkable::Ptr& checkable);
 	void SendAcknowledgementSet(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
 	void SendAcknowledgementCleared(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, double ackLastChange);
+	void SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	void SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	void SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+	void SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	void SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	template<class T>
+	void SendGroupsChanged(const ConfigObject::Ptr& command, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	void SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+	void SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+	void SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
 
 	std::vector<String> UpdateObjectAttrs(const ConfigObject::Ptr& object, int fieldType, const String& typeNameOverride);
 	Dictionary::Ptr SerializeState(const Checkable::Ptr& checkable);
@@ -104,11 +116,13 @@ private:
 	static String FormatCommandLine(const Value& commandLine);
 	static long long TimestampToMilliseconds(double timestamp);
 	static String IcingaToStreamValue(const Value& value);
+	static std::vector<Value> GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew);
+	static std::vector<String> GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew);
 
-	static ArrayData GetObjectIdentifiersWithoutEnv(const ConfigObject::Ptr& object);
 	static String GetObjectIdentifier(const ConfigObject::Ptr& object);
-	static String GetEnvironment();
-	static Dictionary::Ptr SerializeVars(const CustomVarObject::Ptr& object);
+	static String CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime = 0, NotificationType nt = NotificationType(0));
+	static const char* GetNotificationTypeByEnum(NotificationType type);
+	static Dictionary::Ptr SerializeVars(const Dictionary::Ptr& vars);
 
 	static String HashValue(const Value& value);
 	static String HashValue(const Value& value, const std::set<String>& propertiesBlacklist, bool propertiesWhitelist = false);
@@ -134,30 +148,22 @@ private:
 	static void NextCheckChangedHandler(const Checkable::Ptr& checkable);
 	static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
 	static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime);
+	static void NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	static void NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	static void TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+	static void TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	static void TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	static void UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr&, const Array::Ptr& newValues);
+	static void HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	static void ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues);
+	static void CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+	static void CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
+	static void CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
 
 	void AssertOnWorkQueue();
 
 	void ExceptionHandler(boost::exception_ptr exp);
 
-	template<class T>
-	static inline
-	std::vector<T> Prepend(std::vector<T>&& needle, std::vector<T>&& haystack)
-	{
-		for (auto& hay : haystack) {
-			needle.emplace_back(std::move(hay));
-		}
-
-		return std::move(needle);
-	}
-
-	template<class T, class Needle>
-	static inline
-	std::vector<T> Prepend(Needle&& needle, std::vector<T>&& haystack)
-	{
-		haystack.emplace(haystack.begin(), std::forward<Needle>(needle));
-		return std::move(haystack);
-	}
-
 	static std::vector<Type::Ptr> GetTypes();
 
 	Timer::Ptr m_StatsTimer;
@@ -178,7 +184,7 @@ private:
 	} m_DumpedGlobals;
 
 	static String m_EnvironmentId;
-	static boost::once_flag m_EnvironmentIdOnce;
+	static std::once_flag m_EnvironmentIdOnce;
 };
 }
 


=====================================
lib/icingadb/icingadb.ti
=====================================
@@ -44,6 +44,10 @@ class IcingaDB : ConfigObject
 	[config] double connect_timeout {
 		default {{{ return DEFAULT_CONNECT_TIMEOUT; }}}
 	};
+
+	[no_storage] String environment_id {
+			get;
+	};
 };
 
 }


=====================================
lib/icingadb/redisconnection.cpp
=====================================
@@ -116,7 +116,7 @@ void LogQuery(RedisConnection::Query& query, Log& msg)
 void RedisConnection::FireAndForgetQuery(RedisConnection::Query query, RedisConnection::QueryPriority priority)
 {
 	{
-		Log msg (LogNotice, "IcingaDB", "Firing and forgetting query:");
+		Log msg (LogDebug, "IcingaDB", "Firing and forgetting query:");
 		LogQuery(query, msg);
 	}
 
@@ -138,7 +138,7 @@ void RedisConnection::FireAndForgetQuery(RedisConnection::Query query, RedisConn
 void RedisConnection::FireAndForgetQueries(RedisConnection::Queries queries, RedisConnection::QueryPriority priority)
 {
 	for (auto& query : queries) {
-		Log msg (LogNotice, "IcingaDB", "Firing and forgetting query:");
+		Log msg (LogDebug, "IcingaDB", "Firing and forgetting query:");
 		LogQuery(query, msg);
 	}
 
@@ -162,7 +162,7 @@ void RedisConnection::FireAndForgetQueries(RedisConnection::Queries queries, Red
 RedisConnection::Reply RedisConnection::GetResultOfQuery(RedisConnection::Query query, RedisConnection::QueryPriority priority)
 {
 	{
-		Log msg (LogNotice, "IcingaDB", "Executing query:");
+		Log msg (LogDebug, "IcingaDB", "Executing query:");
 		LogQuery(query, msg);
 	}
 
@@ -192,7 +192,7 @@ RedisConnection::Reply RedisConnection::GetResultOfQuery(RedisConnection::Query
 RedisConnection::Replies RedisConnection::GetResultsOfQueries(RedisConnection::Queries queries, RedisConnection::QueryPriority priority)
 {
 	for (auto& query : queries) {
-		Log msg (LogNotice, "IcingaDB", "Executing query:");
+		Log msg (LogDebug, "IcingaDB", "Executing query:");
 		LogQuery(query, msg);
 	}
 


=====================================
test/CMakeLists.txt
=====================================
@@ -70,6 +70,7 @@ add_boost_test(base
     base_dictionary/remove
     base_dictionary/clone
     base_dictionary/json
+    base_dictionary/keys_ordered
     base_fifo/construct
     base_fifo/io
     base_json/encode


=====================================
test/base-dictionary.cpp
=====================================
@@ -3,6 +3,8 @@
 #include "base/dictionary.hpp"
 #include "base/objectlock.hpp"
 #include "base/json.hpp"
+#include "base/string.hpp"
+#include "base/utility.hpp"
 #include <BoostTestTargetConfig.h>
 
 using namespace icinga;
@@ -183,4 +185,16 @@ BOOST_AUTO_TEST_CASE(json)
 	BOOST_CHECK(deserialized->Get("test2") == "hello world");
 }
 
+BOOST_AUTO_TEST_CASE(keys_ordered)
+{
+	Dictionary::Ptr dictionary = new Dictionary();
+
+	for (int i = 0; i < 100; i++) {
+		dictionary->Set(std::to_string(Utility::Random()), Utility::Random());
+	}
+
+	std::vector<String> keys = dictionary->GetKeys();
+	BOOST_CHECK(std::is_sorted(keys.begin(), keys.end()));
+}
+
 BOOST_AUTO_TEST_SUITE_END()


=====================================
tools/mkclass/class_lexer.ll
=====================================
@@ -134,6 +134,7 @@ no_user_view			{ yylval->num = FANoUserView; return T_FIELD_ATTRIBUTE; }
 deprecated			{ yylval->num = FADeprecated; return T_FIELD_ATTRIBUTE; }
 get_virtual			{ yylval->num = FAGetVirtual; return T_FIELD_ATTRIBUTE; }
 set_virtual			{ yylval->num = FASetVirtual; return T_FIELD_ATTRIBUTE; }
+signal_with_old_value			{ yylval->num = FASignalWithOldValue; return T_FIELD_ATTRIBUTE; }
 virtual				{ yylval->num = FAGetVirtual | FASetVirtual; return T_FIELD_ATTRIBUTE; }
 navigation			{ return T_NAVIGATION; }
 validator			{ return T_VALIDATOR; }


=====================================
tools/mkclass/classcompiler.cpp
=====================================
@@ -830,10 +830,10 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&)
 				m_Impl << "void ObjectImpl<" << klass.Name << ">::Set" << field.GetFriendlyName() << "(" << field.Type.GetArgumentType() << " value, bool suppress_events, const Value& cookie)" << std::endl
 					<< "{" << std::endl;
 
-				if (field.Type.IsName || !field.TrackAccessor.empty())
-					m_Impl << "\t" << "Value oldValue = Get" << field.GetFriendlyName() << "();" << std::endl;
+				if (field.Type.IsName || !field.TrackAccessor.empty() || field.Attributes & FASignalWithOldValue)
+					m_Impl << "\t" << "Value oldValue = Get" << field.GetFriendlyName() << "();" << std::endl
+						<< "\t" << "auto *dobj = dynamic_cast<ConfigObject *>(this);" << std::endl;
 
-					
 				if (field.SetAccessor.empty() && !(field.Attributes & FANoStorage))
 					m_Impl << "\t" << "m_" << field.GetFriendlyName() << ".store(value);" << std::endl;
 				else
@@ -841,16 +841,22 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&)
 
 				if (field.Type.IsName || !field.TrackAccessor.empty()) {
 					if (field.Name != "active") {
-						m_Impl << "\t" << "auto *dobj = dynamic_cast<ConfigObject *>(this);" << std::endl
-							<< "\t" << "if (!dobj || dobj->IsActive())" << std::endl
+						m_Impl << "\t" << "if (!dobj || dobj->IsActive())" << std::endl
 							<< "\t";
 					}
 
 					m_Impl << "\t" << "Track" << field.GetFriendlyName() << "(oldValue, value);" << std::endl;
 				}
 
-				m_Impl << "\t" << "if (!suppress_events)" << std::endl
-					<< "\t\t" << "Notify" << field.GetFriendlyName() << "(cookie);" << std::endl
+				m_Impl << "\t" << "if (!suppress_events) {" << std::endl
+					<< "\t\t" << "Notify" << field.GetFriendlyName() << "(cookie);" << std::endl;
+
+				if (field.Attributes & FASignalWithOldValue) {
+					m_Impl << "\t\t" << "if (!dobj || dobj->IsActive())" << std::endl
+						   << "\t\t\t" << "On" << field.GetFriendlyName() << "ChangedWithOldValue(static_cast<" << klass.Name << " *>(this), oldValue, value);" << std::endl;
+				}
+
+				m_Impl << "\t" "}" << std::endl << std::endl
 					<< "}" << std::endl << std::endl;
 			}
 		}
@@ -1053,6 +1059,15 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&)
 		for (const Field& field : klass.Fields) {
 			m_Header << "\t" << "static boost::signals2::signal<void (const intrusive_ptr<" << klass.Name << ">&, const Value&)> On" << field.GetFriendlyName() << "Changed;" << std::endl;
 			m_Impl << std::endl << "boost::signals2::signal<void (const intrusive_ptr<" << klass.Name << ">&, const Value&)> ObjectImpl<" << klass.Name << ">::On" << field.GetFriendlyName() << "Changed;" << std::endl << std::endl;
+
+			if (field.Attributes & FASignalWithOldValue) {
+				m_Header << "\t" << "static boost::signals2::signal<void (const intrusive_ptr<" << klass.Name
+					<< ">&, const Value&, const Value&)> On" << field.GetFriendlyName() << "ChangedWithOldValue;"
+					<< std::endl;
+				m_Impl << std::endl << "boost::signals2::signal<void (const intrusive_ptr<" << klass.Name
+					<< ">&, const Value&, const Value&)> ObjectImpl<" << klass.Name << ">::On"
+					<< field.GetFriendlyName() << "ChangedWithOldValue;" << std::endl << std::endl;
+			}
 		}
 	}
 


=====================================
tools/mkclass/classcompiler.hpp
=====================================
@@ -60,7 +60,8 @@ enum FieldAttribute
 	FADeprecated = 4096,
 	FAGetVirtual = 8192,
 	FASetVirtual = 16384,
-	FAActivationPriority = 32768
+	FAActivationPriority = 32768,
+	FASignalWithOldValue = 65536,
 };
 
 struct FieldType



View it on GitLab: https://salsa.debian.org/nagios-team/pkg-icinga2/-/commit/ed7c657aea415234ac5ba145893c1c8352ae3519

-- 
View it on GitLab: https://salsa.debian.org/nagios-team/pkg-icinga2/-/commit/ed7c657aea415234ac5ba145893c1c8352ae3519
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-nagios-changes/attachments/20211113/54eaea63/attachment-0001.htm>


More information about the pkg-nagios-changes mailing list