Skip to content

Commit 7fd4df6

Browse files
patrickmannclaude
authored andcommitted
Fix extractors being deleted when an input is started or stopped (#26198)
* Fix extractors being deleted when an input is started or stopped The PersistedImpl -> AutoValue migration of InputImpl (#24057) dropped the embedded extractors array from the entity model: the builder ignores unknown properties, so loading an input discards its extractors, and saving uses replaceOne, which then persists the loss by replacing the whole document. Starting or stopping an input persists the desired state on the input document since #25338, so every start/stop deleted all extractors of the input. Updating an input through the REST API had the same effect via the getFields() merge in InputsResource, which also omitted extractors. Fix this by modeling the embedded extractor documents on InputImpl (like the embedded static fields) so they survive full document round-trips, and by carrying them through getFields() and buildFromMap(). Also turn persistDesiredState() into a targeted update of the desired_state field so state changes no longer rewrite the whole document. Fixes #26009 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * CL --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a05a612 commit 7fd4df6

4 files changed

Lines changed: 86 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type = "fixed"
2+
message = "Fix extractors being deleted when an input is started, stopped, or updated."
3+
4+
issues = ["26009"]
5+
pulls = ["26198"]

graylog2-server/src/main/java/org/graylog2/inputs/InputImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ public Map<String, String> getStaticFields() {
120120
return result;
121121
}
122122

123+
/**
124+
* The embedded extractor documents. They are modified through targeted update operations (see
125+
* {@code InputServiceImpl#addExtractor} etc.) and only modeled here so that they survive full document
126+
* replacements when saving an input.
127+
*/
128+
@Nullable
129+
@JsonProperty(EMBEDDED_EXTRACTORS)
130+
public abstract List<Map<String, Object>> getEmbeddedExtractors();
131+
123132
@NotNull
124133
@JsonProperty(FIELD_TYPE)
125134
public abstract String getType();
@@ -179,6 +188,9 @@ public static Builder create() {
179188
@JsonProperty(EMBEDDED_STATIC_FIELDS)
180189
public abstract Builder setEmbeddedStaticFields(List<Map<String, String>> staticFields);
181190

191+
@JsonProperty(EMBEDDED_EXTRACTORS)
192+
public abstract Builder setEmbeddedExtractors(List<Map<String, Object>> extractors);
193+
182194
@JsonProperty(FIELD_TYPE)
183195
public abstract Builder setType(String type);
184196

@@ -226,6 +238,11 @@ public Map<String, Object> getFields() {
226238
doc.put(EMBEDDED_STATIC_FIELDS, getEmbeddedStaticFields());
227239
}
228240

241+
final List<Map<String, Object>> extractors = getEmbeddedExtractors();
242+
if (extractors != null && !extractors.isEmpty()) {
243+
doc.put(EMBEDDED_EXTRACTORS, extractors);
244+
}
245+
229246
if (getContentPack() != null) {
230247
doc.put(FIELD_CONTENT_PACK, getContentPack());
231248
}

graylog2-server/src/main/java/org/graylog2/inputs/InputServiceImpl.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ private InputImpl.Builder buildFromMap(Map<String, Object> fields) {
296296
builder.setEmbeddedStaticFields(staticFields);
297297
}
298298

299+
final List<Map<String, Object>> extractors = (List<Map<String, Object>>) fields.get(InputImpl.EMBEDDED_EXTRACTORS);
300+
if (extractors != null && !extractors.isEmpty()) {
301+
builder.setEmbeddedExtractors(extractors);
302+
}
303+
299304
if (!isGlobal) {
300305
builder.setNodeId((String) fields.get(MessageInput.FIELD_NODE_ID));
301306
}
@@ -762,8 +767,12 @@ private InputImpl withEncryptedFields(InputImpl input) {
762767

763768
@Override
764769
public void persistDesiredState(Input input, IOState.Type desiredState) throws ValidationException {
765-
final Input updatedInput = input.withDesiredState(desiredState);
766-
saveWithoutEvents(updatedInput);
770+
// Use a targeted update instead of saving the whole input to avoid overwriting concurrent changes
771+
// to other parts of the input document.
772+
collection.updateOne(
773+
MongoUtils.idEq(input.getId()),
774+
Updates.set(InputImpl.FIELD_DESIRED_STATE, desiredState.name())
775+
);
767776
}
768777

769778
@Override

graylog2-server/src/test/java/org/graylog2/inputs/InputServiceImplTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ public void persistedDocumentContainsOnlyExpectedFields(MongoCollections mongoCo
448448
.setEmbeddedStaticFields(List.of(
449449
Map.of(InputImpl.FIELD_STATIC_FIELD_KEY, "static_key",
450450
InputImpl.FIELD_STATIC_FIELD_VALUE, "static_value")))
451+
.setEmbeddedExtractors(List.of(createCopyInputExtractor().getPersistedFields()))
451452
.build();
452453

453454
final String id = inputService.save(input);
@@ -466,12 +467,64 @@ public void persistedDocumentContainsOnlyExpectedFields(MongoCollections mongoCo
466467
InputImpl.FIELD_GLOBAL,
467468
InputImpl.FIELD_CONFIGURATION,
468469
InputImpl.EMBEDDED_STATIC_FIELDS,
470+
InputImpl.EMBEDDED_EXTRACTORS,
469471
InputImpl.FIELD_DESIRED_STATE,
470472
InputImpl.FIELD_CONTENT_PACK,
471473
InputImpl.FIELD_NODE_ID
472474
);
473475
}
474476

477+
/**
478+
* Regression test for <a href="https://github.com/Graylog2/graylog2-server/issues/26009">#26009</a>:
479+
* persisting the desired state when starting or stopping an input must not delete its extractors.
480+
*/
481+
@Test
482+
void persistDesiredStateKeepsExtractors() throws Exception {
483+
final Input input = inputService.find(inputService.save(createTestInput()));
484+
inputService.addExtractor(input, createCopyInputExtractor());
485+
486+
inputService.persistDesiredState(input, IOState.Type.STOPPED);
487+
488+
assertThat(inputService.extractorCountByInputId(List.of(input.getId())))
489+
.containsEntry(input.getId(), 1);
490+
assertThat(inputService.find(input.getId()).getDesiredState()).isEqualTo(IOState.Type.STOPPED);
491+
}
492+
493+
/**
494+
* Regression test for <a href="https://github.com/Graylog2/graylog2-server/issues/26009">#26009</a>:
495+
* saving an input that was loaded from the database must round-trip the embedded extractors.
496+
*/
497+
@Test
498+
void saveKeepsExtractors() throws Exception {
499+
final Input input = inputService.find(inputService.save(createTestInput()));
500+
inputService.addExtractor(input, createCopyInputExtractor());
501+
502+
inputService.save(inputService.find(input.getId()));
503+
504+
assertThat(inputService.extractorCountByInputId(List.of(input.getId())))
505+
.containsEntry(input.getId(), 1);
506+
}
507+
508+
/**
509+
* Regression test for <a href="https://github.com/Graylog2/graylog2-server/issues/26009">#26009</a>:
510+
* the input update flow in {@code InputsResource} merges via the {@code getFields()} Map view and
511+
* re-creates the input from the merged map. Extractors must survive this round-trip, too.
512+
*/
513+
@Test
514+
void updateViaFieldsMapKeepsExtractors() throws Exception {
515+
final Input input = inputService.find(inputService.save(createTestInput()));
516+
inputService.addExtractor(input, createCopyInputExtractor());
517+
518+
final Input reloaded = inputService.find(input.getId());
519+
final Map<String, Object> mergedFields = new HashMap<>(reloaded.getFields());
520+
mergedFields.put(MessageInput.FIELD_TITLE, "updated title");
521+
inputService.update(inputService.create(reloaded.getId(), mergedFields));
522+
523+
assertThat(inputService.extractorCountByInputId(List.of(input.getId())))
524+
.containsEntry(input.getId(), 1);
525+
assertThat(inputService.find(input.getId()).getTitle()).isEqualTo("updated title");
526+
}
527+
475528
@Test
476529
void totalExtractorCountByTypeCountsExtractorsAcrossInputs() throws Exception {
477530
final Input input1 = inputService.find(inputService.save(createTestInput()));

0 commit comments

Comments
 (0)