Skip to content

Commit

Permalink
feat: trigger on config_changes update (#1022)
Browse files Browse the repository at this point in the history
* feat: trigger on config_changes update

* feat: add on insert trigger on config changes

* chore: address review comments

* Update views/030_config_changes.sql

---------

Co-authored-by: Moshe Immerman <[email protected]>
  • Loading branch information
adityathebe and moshloop authored Sep 9, 2024
1 parent debc16b commit 8c8011f
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 0 deletions.
65 changes: 65 additions & 0 deletions tests/config_changes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package tests
import (
"time"

"github.com/flanksource/duty/db"
"github.com/flanksource/duty/models"
"github.com/flanksource/duty/query"
"github.com/flanksource/duty/tests/fixtures/dummy"
"github.com/flanksource/duty/types"
"github.com/google/uuid"
ginkgo "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -366,3 +368,66 @@ var _ = ginkgo.Describe("Config changes recursive", ginkgo.Ordered, func() {
})
})
})

var _ = ginkgo.Describe("handle external id conflict on config change inserts", ginkgo.Ordered, func() {
// An arbitrarily chosen time of the event we will be inserting multiple times
var referenceTime = time.Date(2020, 01, 15, 12, 00, 00, 00, time.UTC)

dummyChanges := []models.ConfigChange{
{ConfigID: dummy.LogisticsAPIDeployment.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), Count: 1, CreatedAt: lo.ToPtr(referenceTime.Add(-time.Minute * 5)), Details: []byte(`{"replicas": "1"}`)},
{ConfigID: dummy.LogisticsAPIDeployment.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_2"), Count: 1, CreatedAt: lo.ToPtr(referenceTime.Add(-time.Minute * 4)), Details: []byte(`{"replicas": "2"}`)},
}

ginkgo.BeforeAll(func() {
err := DefaultContext.DB().Create(dummyChanges).Error
Expect(err).To(BeNil())
})

ginkgo.AfterAll(func() {
err := DefaultContext.DB().Delete(dummyChanges).Error
Expect(err).To(BeNil())
})

ginkgo.It("should increase count when the details is changed", func() {
c := models.ConfigChange{ConfigID: dummy.LogisticsAPIDeployment.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), Count: 1, CreatedAt: lo.ToPtr(referenceTime), Details: []byte(`{"replicas": "3"}`)}
err := DefaultContext.DB().Create(&c).Error
Expect(err).To(BeNil())

{
var inserted models.ConfigChange
err := DefaultContext.DB().Where("external_change_id = ? AND config_id = ?", c.ExternalChangeID, c.ConfigID).First(&inserted).Error
Expect(db.ErrorDetails(err)).NotTo(HaveOccurred())
Expect(inserted.Count).To(Equal(2))
}
})

ginkgo.It("should NOT increase count", func() {
// insert the same change with the same details and external change id
// and ensure the count doesn't change.
for i := 0; i < 10; i++ {
c := models.ConfigChange{ConfigID: dummy.LogisticsAPIDeployment.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), CreatedAt: lo.ToPtr(referenceTime), Count: 1, Details: []byte(`{"replicas": "3"}`)}
err := DefaultContext.DB().Create(&c).Error
Expect(err).To(BeNil())

{
var inserted models.ConfigChange
err := DefaultContext.DB().Where("external_change_id = ? AND config_id = ?", c.ExternalChangeID, c.ConfigID).First(&inserted).Error
Expect(db.ErrorDetails(err)).NotTo(HaveOccurred())
Expect(inserted.Count).To(Equal(2))
}
}
})

ginkgo.It("should increase count when the details is the same but the created_at is changed", func() {
c := models.ConfigChange{ConfigID: dummy.LogisticsAPIDeployment.ID.String(), ExternalChangeID: lo.ToPtr("conflict_test_1"), Count: 1, CreatedAt: lo.ToPtr(referenceTime.Add(time.Minute)), Details: []byte(`{"replicas": "3"}`)}
err := DefaultContext.DB().Create(&c).Error
Expect(err).To(BeNil())

{
var inserted models.ConfigChange
err := DefaultContext.DB().Where("external_change_id = ? AND config_id = ?", c.ExternalChangeID, c.ConfigID).First(&inserted).Error
Expect(db.ErrorDetails(err)).NotTo(HaveOccurred())
Expect(inserted.Count).To(Equal(3))
}
})
})
136 changes: 136 additions & 0 deletions views/030_config_changes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
CREATE OR REPLACE FUNCTION config_changes_update_trigger()
RETURNS TRIGGER AS $$
DECLARE
count_increment INT;
BEGIN
count_increment := NEW.count - OLD.count;

UPDATE
config_changes
SET
change_type = NEW.change_type,
count = CASE
WHEN NEW.details IS DISTINCT FROM OLD.details THEN NEW.count
ELSE count
END,
created_at = NOW(),
created_by = NEW.created_by,
details = NEW.details,
is_pushed = NEW.is_pushed,
diff = NEW.diff,
external_created_by = NEW.external_created_by,
external_change_id = NEW.external_change_id,
first_observed = LEAST(first_observed, created_at),
patches = NEW.patches,
severity = NEW.severity,
source = NEW.source,
summary = NEW.summary
WHERE
id = NEW.id;

-- Prevent the original update by returning NULL
RETURN NULL;
EXCEPTION
WHEN unique_violation THEN
IF sqlerrm LIKE '%config_changes_config_id_external_change_id_key%' THEN
UPDATE config_changes
SET
change_type = NEW.change_type,
count = CASE
WHEN NEW.details IS DISTINCT FROM OLD.details THEN config_changes.count + count_increment
ELSE count
END,
created_at = NOW(),
created_by = NEW.created_by,
details = NEW.details,
diff = NEW.diff,
external_created_by = NEW.external_created_by,
first_observed = LEAST(first_observed, created_at),
patches = NEW.patches,
severity = NEW.severity,
source = NEW.source,
summary = NEW.summary
WHERE
external_change_id = NEW.external_change_id AND config_id = NEW.config_id;

RETURN NULL;
ELSE
RAISE;
END IF;
WHEN OTHERS THEN
RAISE;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER config_changes_update_trigger
BEFORE UPDATE
ON config_changes FOR EACH ROW
WHEN (pg_trigger_depth() = 0) EXECUTE FUNCTION config_changes_update_trigger();

---
CREATE OR REPLACE FUNCTION config_changes_insert_trigger()
RETURNS TRIGGER AS $$
DECLARE
existing_details JSONB;
existing_created_at TIMESTAMP WITH TIME ZONE;
BEGIN
-- run the original insert manually.
INSERT INTO config_changes SELECT NEW.*
ON CONFLICT (id)
DO UPDATE
SET
details = excluded.details,
created_by = excluded.created_by,
diff = excluded.diff,
external_created_by = excluded.external_created_by,
patches = excluded.patches,
severity = excluded.severity,
fingerprint = excluded.fingerprint,
count = excluded.count,
source = excluded.source,
created_at = excluded.created_at,
summary = excluded.summary;

-- Prevent the original insert by returning NULL
RETURN NULL;
EXCEPTION
WHEN unique_violation THEN
IF sqlerrm LIKE '%config_changes_config_id_external_change_id_key%' THEN
SELECT details, created_at FROM config_changes
WHERE external_change_id = NEW.external_change_id AND config_id = NEW.config_id
INTO existing_details, existing_created_at;

UPDATE config_changes
SET
change_type = NEW.change_type,
count = CASE
WHEN (NEW.details IS DISTINCT FROM existing_details OR NEW.created_at IS DISTINCT FROM existing_created_at) THEN config_changes.count + 1
ELSE count
END,
created_at = NEW.created_at,
created_by = NEW.created_by,
details = NEW.details,
diff = NEW.diff,
external_created_by = NEW.external_created_by,
patches = NEW.patches,
severity = NEW.severity,
source = NEW.source,
summary = NEW.summary
WHERE
external_change_id = NEW.external_change_id
AND config_id = NEW.config_id;

RETURN NULL;
ELSE
RAISE;
END IF;
WHEN OTHERS THEN
RAISE;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER config_changes_insert_trigger
BEFORE INSERT
ON config_changes FOR EACH ROW
WHEN (pg_trigger_depth() = 0) EXECUTE FUNCTION config_changes_insert_trigger();
---

0 comments on commit 8c8011f

Please sign in to comment.