На работе в рамках проектирования новой системы интеграции устройств для мониторинга аудио/видео потоков возникла задача отслеживания, накопления и последующего анализа изменений их состояния. Состояние выдаётся через зоопарк динамических XML-документов, используемых, в основном, для наполнения legacy web-UI.

Для упрощения интеграции мною была предложена идея создания обобщённой библиотеки для сохранения структурированных diff-ов для (почти) произвольного XML. Поскольку эти diff-ы будут сохраняться с учётом структуры документа, это дало бы возможность очень экономно аккумулировать изменения состояния устройств, а также в будущем генерировать отчёты с аналитикой, диаграммами, и т.п. После недели запойного программирования я набросал работающий proof-of-concept, которым и хочу поделиться в данной статье.

Создание схемы документа


Библиотека использует XSD в качестве источника информации о структуре документа. Получить XSD очень просто: есть много online-сервисов, позволяющих по XML сгенерировать некоторый валидирующий его XSD. Для большинства случаев этого будет достаточно.

Далее требуется слегка модифицировать полученную XSD-схему. Для каждого элемента исходного XML-документа, предполагающего множественные вхождения, требуется добавить атрибут `monId` в соответствующий XSD `element`. Его значением будет имя атрибута, однозначно идентифицирующего повторяющийся элемент. Например, мы собираемся мониторить документы следующего вида:

<element1>
    <element2 attr1="value1">
        <element3>
            <element4 attr2="value2">value3</element4>
            <element4 attr2="value4">value5</element4>
            <element4 attr2="value6">value7</element4>
        </element3>
    </element2>
    <element2 attr1="value8">
        <element3>
            <element4 attr2="value9">value10</element4>
            <element4 attr2="value11">value12</element4>
        </element3>
    </element2>
</element1>

По структуре документа понятно, что как минимум следующие элементы имеют множественное вхождение:

  • /element1/element2
  • /element1/element2/element3/element4

Поэтому в соответствующие XSD `elements` должны быть добавлены `monId` с именами идентифицирующих атрибутов:

<xs:element name=«element2» maxOccurs=«unbounded» minOccurs=«0» monId=«attr1»>

<xs:element name=«element4» maxOccurs=«unbounded» minOccurs=«0» monId=«attr2»>


Как это работает


Итак, библиотека парсит XSD (на самом деле, пока поддерживается только его ограниченное подмножество, достаточное для переваривания большинства автоматически сгенерированных схем), и на его основе создаёт таблицы, соотвествующие элементам исходного документа.



После создания внутреннего представления схемы документа каждому его элементу будет соответствовать таблица в базе данных. Любое изменение элемента приведёт к добавлению новой записи в такой таблице. Т.е. каждая запись означает некоторое событие (добавление, изменение, удаление, snapshot). Другими словами, для извлечения версии документа, соответствующей заданной временной метке, библиотека сканирует все события, соответствующие данному элементу, и реконструирует его состояние.

Поскольку событий может быть множество, такая реконструкция будет требовать всё больше и больше времени. Вот почему для каждого документа периодически требуется сохранять снимок его текущего состояния (snapshot). Таким образом, реконструкция элементов будет производиться не с начала существования документа, а с ближайшего snapshot-а для указанной временной метки.

Использование


Библиотека написана на golang и хранит документы в PostgreSQL. В качестве драйвера базы данных используется libpq. В текущем состоянии библиотека умеет только сохранять и реконструировать XML-документы (для произвольной временной метки).

Пример использования
package main

import (
	"btc/data"
	"btc/mon"
	"btc/xmls"
	"database/sql"
	"log"
	"os"
	"time"
)

func install(db *sql.DB) {
	var err error
	if err = mon.Install(db); err != nil {
		log.Fatalf("failed to install data monitor: %s", err)
	}

	var root *xmls.Element
	root, err = xmls.FromFile("tmp/etr.xsd")
	if err != nil {
		log.Fatalf("failed to create xml schema: %s", err)
	}

	schema := mon.NewSchema("etr", "probe ETR-290 checks")
	if err = mon.AddSchema(db, schema, root); err != nil {
		log.Fatalf("failed to install schema: %s", err)
	}

	doc := mon.NewDoc("hw4_172_etr", "etr",
		"http://10.0.30.172/probe/etrdata?inputId=0&tuningSetupId=1",
		60, 86400)
	if err = mon.AddDoc(db, doc); err != nil {
		log.Fatalf("failed to add document: %s", err)
	}
}

func commit(db *sql.DB) {
	file, err := os.Open("tmp/etr.xml")
	if err != nil {
		log.Fatalf("failed to open xml doc: %s", err)
	}
	defer file.Close()

	if err = mon.CommitDoc(db, "hw4_172_etr", file, false); err != nil {
		log.Fatalf("failed to commit doc: %s", err)
	}
}

func checkout(db *sql.DB) {
	timestamp, err := time.Parse(
		time.RFC3339, "2015-12-25T18:26:58+01:00")
	if err != nil {
		log.Fatalf("failed to parse timestamp: %s", err)
	}

	if err := mon.CheckoutDoc(
		db, "hw4_172_etr", timestamp,
		os.Stdout, " ", " "); err != nil {
		log.Fatalf("failed to checkout doc: %s", err)
	}
}

func main() {
	config, err := NewConfig("config.json")
	if err != nil {
		log.Fatalf("failed to load config: %s", err)
	}

	var db *sql.DB
	db, err = data.Open(config.DbConnStr)
	if err != nil {
		log.Fatalf("failed to establish db connection: %s", err)
	}
	defer db.Close()

	//install(db)
	//commit(db)
	checkout(db)
}

Комментарии (2)


  1. Eternalko
    28.12.2015 03:31
    -1

    Мысли на тему:
    — JSON вместо XML
    — JSON Schema вместо XSD (Если вообще нужен)
    — DocumentDB (a la Mongo) вместо RDBMS (Postgres)

    Возможно (возможно) вышло бы меньше кода / проще аналитика.


  1. Santacruz
    28.12.2015 15:49

    https://github.com/elierotenberg/remutable
    https://github.com/intelie/immutable-js-diff