chore: add indictment support to Bavet's Node Network - #2528
chore: add indictment support to Bavet's Node Network#2528Christopher-Chianelli wants to merge 17 commits into
Conversation
This allows support infomation to be automatically passed when a new tuple is created (ex: in map/expand).
…d, add notes about stale ifExists indictments
…oesn't update, make PrecomputeUni tests aware of indictments
… if exists, aggregate support for precompute, make BiPrecomputeTest Indictment aware
|
There was a problem hiding this comment.
First read-through. An elegant solution, was expecting this to be more code; not happy with some bits, though. Let's try to adjust if we can.
Before we move further though, I believe we need to have some clarity on how stable this is. We have the turtle tests - let's run them on this. (They may need some ad-hoc adjusting to be able to run with indictments.) If at the same time we can gather move speed data from them, we can compare to non-indictment runs and have a decent idea as to how much slower this will be.
| var created = createTuple(originalTuple, bag.value); | ||
| created.setIndictmentSource(originalTuple.getIndictmentSource()); |
There was a problem hiding this comment.
Cases like this would benefit from inheriting the indictment source at construction time; the mutability aspect will probably be necessary in some places (haven't yet read the entire thing), but I think the default pattern here should not happen post-construction.
| if (tuple.getIndictmentSource() != IndictmentSource.DISABLED) { | ||
| if (outTuple.getIndictmentSource() == IndictmentSource.DISABLED) { | ||
| outTuple.setIndictmentSource( | ||
| new IndictmentSource.AggregateIndictmentSource(new ArrayList<>(), new LinkedHashMap<>())); |
There was a problem hiding this comment.
This is inconsistent with the otherwise seemingly standard pattern of having a named static factory for these operations.
| */ | ||
| public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, Score_ score) { | ||
| public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, | ||
| @Nullable List<Object> indictedObjects, Score_ score) { |
There was a problem hiding this comment.
I wonder if - instead of dealing with null here - we instead want to use an empty immutable collection here.
| return switch (scoreAnalysisFetchPolicy) { | ||
| case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS; | ||
| case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS; | ||
| case FETCH_ALL -> ENABLED; |
There was a problem hiding this comment.
You could argue this is not backwards compatible. People will get indictments where previously they did not.
Not sure what I think about this yet.
| } | ||
| } | ||
|
|
||
| public void setNodeIds(long[] scorerNodeIds) { |
There was a problem hiding this comment.
Not excited that we're effectively creating more and more shared mutable state. Let's try to keep these things final and supplied at construction time. These "half-initialized" lazy objects make reasoning about the functionality/interactions significantly more difficult.
| var abstractConstraint = (AbstractConstraint<?, ?, ?>) constraint; | ||
| var involvedNodeIds = Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds()); | ||
| tuple.getIndictmentSource().visitSources(involvedNodeIds, out::add); | ||
| return new ArrayList<>(out); |
There was a problem hiding this comment.
I wonder if we need this to be a list. (It used to be one in the old days.) But what would be the actual benefit/need? A SequencedSet works equally well IMO, and avoids this inefficient copy.
SequencedCollection? I'm thinking that List.of(...) may be useful for testing, but new LinkedHashSet<>() is best for performance.



Each tuple now have an
IndictmentSourcefield. This field is set toIndictmentSource.DISABLEDwhen indictments are disabled, which is used to exit early and avoid work only required for indictments.ConstraintMatchPolicyhave two new values:ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTSthis has the previous behaviour ofENABLED_WITHOUT_JUSTIFICATIONS; has constraint matches but no justifications nor indictmentsENABLED_WITHOUT_INDICTMENTSthis includes justifications but not indictmentsENABLEDwas changed to include both justifications and indictments instead of just justifications.ENABLED_WITHOUT_JUSTIFICATIONSwas changed to include indictments.The actual work each node does is pretty simple. Do note that the attach IndictmentSource does not change after an insert:
forEach: if indictments are enabled, set the source of the tuple to aRootIndictmentSource(perhapsLeafIndictmentSourcewould be a better name) on insert. Retract and update do no extra work;retractremoves the tuple (and hence the indictment source and potential constraint match), andupdatedoes not change the instance tracked.filter: not a node, and does not affect what object is indicted.map/expand/flatten: these creates new tuples from a source tuple, and simply copy the source's indictment source on insert. Update and retract do not need to do anything following the same reasoning asforEach`.join: oninsert, set the indictment source of the joined tuple to aJoinedIndictmentSourcethat have a reference to both the left and right tuples' indictment sources.groupBy: oninsert, set the indictment source of the group's tuple to an aggregation containing the indictment sources of the tuples inside the group. Onupdate, remove the tuple's indictment source from the old group aggregation and add it to the new group aggregation. Onretract, remove the tuple's indictment source from the aggregation.distinct: this isgroupByin a trenchcoat and behaves similarly.concat: behaves similarly tomap/expand/flatten, since the left side and right side act independently and don't affect each other.ifExists/ifNotExists: The most complex of the bunch. When indictments are enabled, it takes a drastically different path than normal. In particular, right tuple will propagate updates even if the corresponding left tuple is not update. This is to prevent constraint matches with stale indictments referring to removed/retracted entities/problem facts. Additional tuple iteration occurs for indexedifExistsnodes so the tuple's indictments are up to date when the left tuple changes. Moreover, sinceifExistsdoes not create a new tuple but still adds to the indictment, the same indictment source is used, but the indictment source's support map for the key corresponding to theifExistsnode's id is updated.precompute: Since it uses an independent node network, its node's ids do not correspond to the outer node network's node ids. As such, the indictment support map of the produced tuples must be aggregated along with the original indictment source.Each constraint is now aware of the ids of the nodes that affect the constraint. It uses it when creating constraint matches to iterate through the support map so it does not indict objects from other constraints with
ifExiststhat so happen to share the same tuple. EachIndictmentSourceis iterated in a tree like matter to collect a set of unique indicted objects which is converted to a list inside the actual ConstraintMatch.The vast majority of the changes were making all existing ConstraintStream tests aware of indictments.
assertMatchwas changed so it sets the indictment list to the justifications, which work for some but not all tests. For the other tests,withIndictedObjectsis used to set the expected indicted objects for each match.Note: I am aware of the stale "NOTE: By not propagating here, ..." comment in
IfExistsNodeand will remove it.Note: I am aware that the code under
if (testFiltering(...))inIfExistsNodeshould do a branch depending on if indictments are enabled and call the right method (was written before the change to keep the indictments up to date and the two methods were not too different from each other).Note: I am aware the
carnalityandstoreSizemethods added toTuplecan be removed; they are an artifact from when tuple cloning were used forifExists's indictment enabled path.