You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ignite.apache.org by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org> on 2026/06/22 06:10:14 UTC

[PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

EgorBaranovEnjoysTyping opened a new pull request, #13262:
URL: https://github.com/apache/ignite/pull/13262

   Collect toString as tree, avoid extra allocations and problems in recursion resolution
   Problems solved:
   Any recursive call in toString method would be handled
   Allocate less memory
   Memory wouldn't be kept for a whole thread life


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "anton-vinogradov (via GitHub)" <gi...@apache.org>.
anton-vinogradov commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3483045395


##########
modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java:
##########
@@ -204,6 +205,106 @@ public void testToStringCheckObjectRecursionPrevention() throws Exception {
         fut4.get(3_000);
     }
 
+
+    /** */
+    @Test
+    public void testNPE() {
+        boolean success;
+        try {
+            SBLimitedLength sbLimitedLength = new SBLimitedLength(256);
+            sbLimitedLength.initLimit(new SBLengthLimit());
+            sbLimitedLength.a("a".repeat(7999));
+            sbLimitedLength.i(7000, "asd");
+            sbLimitedLength.a("a".repeat(10));
+            success = true;
+        } catch (NullPointerException ignored) {
+            success = false;
+        }
+        Assert.assertTrue(success);
+    }

Review Comment:
   JUnit 4 isn't the blocker here — Ignite ships its own `GridTestUtils.assertThrows(log, callable, cls, msg)` / `assertThrowsWithCause(...)`, used all over the test base (`CdcSelfTest`, `NodeWithFilterRestartTest`, …). So you can assert the throw directly instead of the `boolean` + `catch (NPE)` bypass, which keeps the full stack trace on failure and won't silently swallow a different exception. (As an aside, JUnit 4.13 also added `org.junit.Assert.assertThrows`.)



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES
+            = new ConcurrentHashMap<>();
+
+    /** Last constructed node. */
+    static final ThreadLocal<GridToStringNode> LAST_CONSTRUCTED_GRID_TO_STRING_NODE = new ThreadLocal<>();
+
+    /** The name of the property this node represents. */
+    String propName;
+
+    /** Inner buffer. For inner calls. */
+    StringBuilder innerBuf = new StringBuilder(0);
+
+    /**
+     * Base constructor.
+     * @param propName The name of the property.
+     */
+    GridToStringNode(String propName) {
+        this.propName = propName;
+    }
+
+    /**
+     * Creates a root node for a string value.
+     * @param str The string value.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(String str, List<GridToStringNode> addNodes) {
+        return new GridToStringObjectNode(str, addNodes);
+    }
+
+    /**
+     * Creates a root node for an object.
+     * Checks for recursion and creates either a termination node or a full object node.
+     * @param obj The object to represent.
+     * @param cls The class of the object.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(Object obj, Class<?> cls, List<GridToStringNode> addNodes) {
+        return recursionTermination(obj)
+                .orElseGet(() -> new GridToStringObjectNode(null, obj, cls, addNodes));
+    }
+
+    /**
+     * Marks a node in the thread-local cache with a unique, empty string key.
+     * Used to handle references during object graph traversal.
+     * @param node The node to mark.
+     * @return The unique marker string.
+     */
+    static String markNode(GridToStringNode node) {
+        String result = new String(GridToStringNode.class.getSimpleName());

Review Comment:
   1) Agreed on `new String(...)` — you do need a fresh identity because `toString()` may hand back an interned/pooled instance. That part is fine.
   
   2) But `Optional.orElseThrow()` is not a Java `assert`: it throws `NoSuchElementException` unconditionally (it's not gated by `-ea`), and that throw escapes from inside `toString()` — exactly the "toString must never throw" failure mode the original blanket try/catch existed to prevent. If the intent is only to document the "init() ran" invariant, use `assert identities().isPresent()` (or `orElseThrow(() -> new AssertionError(...))`), so it's a no-op in production instead of being able to turn a logging call into a thrown exception.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1994,87 +1497,13 @@ public static <T> String joinToString(
         return buf.toString();
     }
 
-    /**
-     * Checks that object is already saved.
-     * In positive case this method inserts hash to the saved object entry (if needed) and name@hash for current entry.
-     * Further toString operations are not needed for current object.
-     *
-     * @param buf String builder buffer.
-     * @param obj Object.
-     * @param cls Class.
-     * @param svdObjs Map with saved objects to handle recursion.
-     * @return {@code True} if object is already saved and name@hash was added to buffer.
-     * {@code False} if it wasn't saved previously and it should be saved.
-     */
-    private static boolean handleRecursion(
-        SBLimitedLength buf,
-        Object obj,
-        @NotNull Class cls,
-        IdentityHashMap<Object, EntryReference> svdObjs
-    ) {
-        EntryReference ref = svdObjs.get(obj);
-
-        if (ref == null)
-            return false;
-
-        int pos = ref.pos;
-
-        String name = cls.getSimpleName();
-        String hash = identity(obj);
-        String savedName = name + hash;
-        String charsAtPos = buf.impl().substring(pos, pos + savedName.length());
-
-        if (!buf.isOverflowed() && !savedName.equals(charsAtPos)) {
-            if (charsAtPos.startsWith(cls.getSimpleName())) {
-                buf.i(pos + name.length(), hash);
-
-                incValues(svdObjs, obj, hash.length());
-            }
-            else
-                ref.hashNeeded = true;
-        }
-
-        buf.a(savedName);
-
-        return true;
-    }
-
-    /**
-     * Increment positions of already presented objects afterward given object.
-     *
-     * @param svdObjs Map with objects already presented in the buffer.
-     * @param obj Object.
-     * @param hashLen Length of the object's hash.
-     */
-    private static void incValues(IdentityHashMap<Object, EntryReference> svdObjs, Object obj, int hashLen) {
-        int baseline = svdObjs.get(obj).pos;
-
-        for (IdentityHashMap.Entry<Object, EntryReference> entry : svdObjs.entrySet()) {
-            EntryReference ref = entry.getValue();
-
-            int pos = ref.pos;
-
-            if (pos > baseline)
-                ref.pos = pos + hashLen;
-        }
-    }
-
-    /**
-     *
-     */
-    private static class EntryReference {
-        /** Position. */
-        int pos;
-
-        /** First object entry needs hash to be written. */
-        boolean hashNeeded;
-
-        /**
-         * @param pos Position.
-         */
-        private EntryReference(int pos) {
-            this.pos = pos;
-            hashNeeded = false;
-        }
+    /** */
+    private static <T extends Throwable> String handleThrowable(T throwable) {
+        StringWriter strWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(strWriter);
+        throwable.printStackTrace(printWriter);
+        printWriter.flush();
+        printWriter.close();
+        return strWriter.toString();
     }

Review Comment:
   This is actually a behavioral change from master: there the catch did `throw new IgniteException(e)` ("No other option here") — it propagated the failure, it did not render the exception into the result. `handleThrowable` now returns the **full stack trace as the string value**, so a failing `toString()` dumps a multi-line trace into wherever the value was being logged (and can surface internal/sensitive detail — Copilot's point). I'd either keep master's rethrow, or return a short `cls + ": " + message` summary and let the trace go through the logger, rather than inlining the whole trace as the field value.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES

Review Comment:
   The `finally`-clear handles the *leak* (given perfectly balanced init/clear), but the design point stands: this map is only ever touched by its owning thread, so `ConcurrentHashMap<Thread, …>` is pure overhead over a `ThreadLocal` — and `LAST_CONSTRUCTED_…` / `OBJECT_REGISTRY` right next to it are already `ThreadLocal`. A `ThreadLocal<IdentityHashMap<…>>` is leak-free by construction and removes the per-call thread lookup. Also the `CATCHED` → `CACHED` rename (Copilot's nit) doesn't look applied yet.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "anton-vinogradov (via GitHub)" <gi...@apache.org>.
anton-vinogradov commented on PR #13262:
URL: https://github.com/apache/ignite/pull/13262#issuecomment-4811808585

   Thanks for the round of fixes — re-reviewed at `7c75b1a`.
   
   **The main blocker is genuinely resolved.** `markNode` now returns the real `node.toString()` (wrapped in `new String(...)` for identity) instead of the literal `GridToStringNode` placeholder, so the dangerous `"Foo{" + S.toString(...) + "}"` concatenation case now degrades to the correct content rather than corrupting the log. I verified parity with master:
   
   ```
   wrapped  : Parent [child=WRAP[Child [v=42]], name=p]
   unwrapped: Parent2 [child=PlainChild [v=7]]
   ```
   
   Cycle handling and the original NPE repro (self-ref sweep across the `HEAD_LEN` boundary, pad 7975–7985) also pass with no NPE. Dropping `recoverObject` and the typo/placeholder fixes look good. 👍
   
   A few things still open:
   
   1. **`CATCHED_NODES` (`GridToStringNode:38`) is still `static ConcurrentHashMap<Thread, …>`.** Your "`clear()` is always in `finally`" answer addresses the *leak*, but not the *design*: this map is only ever touched by its owning thread, and `LAST_CONSTRUCTED_…` / `OBJECT_REGISTRY` in the same class are already `ThreadLocal`. A `ThreadLocal` here is leak-free by construction and drops the per-call `Thread`-keyed lookup — please make it consistent.
   
   2. **`markNode` eagerly materializes the whole child subtree into a String** (`new String(node.toString())`) just to use it as a map key. That's exactly the eagerness that weakens the point of `SBLimitedLength` — bounding the *cost* of `toString` on large graphs, not just the output length. The streaming baseline stopped doing work once the head filled; this renders every nested `S.toString` in full first. I'd still like a JMH before/after on a large/deep graph (or at least an explicit acknowledgement of the trade-off in the ticket).
   
   3. **`SBLimitedLength.i(int,String)` else-branch still dereferences `tail` with no guard** (`:277`). The "offset ≥ head ⇒ tail already exists" invariant is plausible, but it's implicit. Cheapest fix is to document it with `assert tail != null` so a future change can't silently reintroduce the NPE class this PR is fixing.
   
   On the bigger picture: I understand you'd rather frame this as "`handleRecursion` is incorrect" than "fix the NPE", and the rewrite is meaningfully safer now. But the core concern stands — this is a ~1700-line architectural rewrite of the `toString` engine (two parallel nested-call mechanisms, `markNode` vs `appendInnerBuffer`) riding on an NPE ticket, with an unmeasured perf change on exactly the large-graph path the limiter exists for. I still think the cleanest path is the localized `SBLimitedLength` insert fix + regression test for this ticket, and the node-tree as a separate change with a JMH story — but that's a maintainer call, so I'll defer to the others on scope.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481768672


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES
+            = new ConcurrentHashMap<>();
+
+    /** Last constructed node. */
+    static final ThreadLocal<GridToStringNode> LAST_CONSTRUCTED_GRID_TO_STRING_NODE = new ThreadLocal<>();
+
+    /** The name of the property this node represents. */
+    String propName;
+
+    /** Inner buffer. For inner calls. */
+    StringBuilder innerBuf = new StringBuilder(0);
+
+    /**
+     * Base constructor.
+     * @param propName The name of the property.
+     */
+    GridToStringNode(String propName) {
+        this.propName = propName;
+    }
+
+    /**
+     * Creates a root node for a string value.
+     * @param str The string value.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(String str, List<GridToStringNode> addNodes) {
+        return new GridToStringObjectNode(str, addNodes);
+    }
+
+    /**
+     * Creates a root node for an object.
+     * Checks for recursion and creates either a termination node or a full object node.
+     * @param obj The object to represent.
+     * @param cls The class of the object.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(Object obj, Class<?> cls, List<GridToStringNode> addNodes) {
+        return recursionTermination(obj)
+                .orElseGet(() -> new GridToStringObjectNode(null, obj, cls, addNodes));
+    }
+
+    /**
+     * Marks a node in the thread-local cache with a unique, empty string key.
+     * Used to handle references during object graph traversal.
+     * @param node The node to mark.
+     * @return The unique marker string.
+     */
+    static String markNode(GridToStringNode node) {
+        String result = new String(GridToStringNode.class.getSimpleName());
+        identities()
+                .orElseThrow()
+                .put(result, node);
+        return result;
+    }
+
+    /**
+     * Appends inner buffer to last created node.
+     * @param node Node - new node to fill inner buffer.
+     */
+    static String appendInnerBuffer(GridToStringNode node) {
+        GridToStringNode parent = LAST_CONSTRUCTED_GRID_TO_STRING_NODE.get();
+        parent.innerBuf.append(node.toString());
+        return "";
+    }
+
+    /**
+     * Checks if the current context is new, meaning no recursion or inner calls are in progress.
+     * @return True if the context is new; false otherwise.
+     */
+    static boolean init() {
+        Thread curThread = Thread.currentThread();
+        boolean containsThread = CATCHED_NODES.containsKey(curThread);
+        if (!containsThread)
+            CATCHED_NODES.put(curThread, new IdentityHashMap<>());
+        return !containsThread;
+    }
+
+    /**
+     * Creates a node for a null value if the object is null.
+     * @param obj The object to check.
+     * @return An Optional containing a null node if the object is null; otherwise, an empty Optional.
+     */
+    static Optional<GridToStringNode> nullNode(Object obj) {
+        return obj == null ? Optional.of(new GridToStringNullNode(null)) : Optional.empty();
+    }
+
+    /**
+     * Checks if an object is part of a recursive reference and, if so,
+     * creates a termination node to break the cycle.
+     * @param obj The object to check.
+     * @return An Optional containing a termination node if recursion is detected;
+     * otherwise, an empty Optional.
+     */
+    static Optional<GridToStringNode> recursionTermination(Object obj) {
+        return Optional.ofNullable(obj)
+                .flatMap(NodeRecursionMonitor::findRecursionMonitor)
+                .map(sameObjNode ->
+                        GridToStringRecursionTerminationNode.of(sameObjNode, obj));
+    }
+
+    /**
+     * Appends the property name (if any) and a placeholder for the value to the builder.
+     * This method is intended to be overridden by subclasses to provide specific value output.
+     * @param sb The string builder to append to.
+     */
+    void appendNode(GridStringBuilder sb) {
+        if (propName != null)
+            sb.a(propName).a("=");
+    }
+
+    /**
+     * Clears the thread-local cache of nodes and recursion prevention storage.
+     */
+    static void clear() {
+        CATCHED_NODES.remove(Thread.currentThread());
+        OBJECT_REGISTRY.remove();
+        LAST_CONSTRUCTED_GRID_TO_STRING_NODE.remove();
+    }
+
+    /**
+     * Generates the final string representation of this node.
+     * Initializes a limited-length string builder, appends the node's content,
+     * clears the cache, and returns the result.
+     * @return The string representation of the node.
+     */
+    @Override public String toString() {
+        SBLimitedLength sb = new SBLimitedLength(256);
+        sb.initLimit(new SBLengthLimit());
+        appendNode(sb);
+        return sb.toString();
+    }
+
+    /** Retrieves identity hash map if it was initialized earlier
+     * @return Optional identity hash map that stores catched nodes
+     * */
+    static Optional<IdentityHashMap<String, GridToStringNode>> identities() {
+        return Optional.ofNullable(CATCHED_NODES.get(Thread.currentThread()));
+    }
+
+    /**
+     * If someone decided to concat outside of default
+     * {@link org.apache.ignite.internal.util.typedef.internal.S#toString}
+     * we should replace the substring
+     * @param obj Object.
+     */
+    public static <T> T recoverObject(T obj) {

Review Comment:
   Already solved, it was designed for cases when inner toString result is mutated in outer call with GridStringBuilder.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481294689


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java:
##########
@@ -170,13 +157,102 @@ private CircularStringBuilder appendNull() {
         return this;
     }
 
+    /**
+     * Inserts a string into the buffer at the specified logical offset.
+     * This method is optimized to minimize the number of elements moved by choosing
+     * to shift elements from the closest end (left or right) to the insertion point.
+     *
+     * @param offset      The logical position (accounting for skipped characters)
+     *                    at which to insert.
+     * @param valToInsert The string to be inserted.
+     * @throws StringIndexOutOfBoundsException if the offset is invalid.
+     */
+    public void insert(int offset, String valToInsert) {
+        int curLength = length();
+        int offsetInsideBuf = offset - skipped;
+        if (offset < 0 || offsetInsideBuf > curLength)
+            throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength);
+        if (valToInsert == null)
+            valToInsert = "null";
+        int insertLength = valToInsert.length();
+        if (insertLength == 0)
+            return;
+        if (offsetInsideBuf == curLength) {
+            append(valToInsert);
+            return;
+        }
+        int spareSpace = value.length - curLength;
+        int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf);
+        if (insertCnt <= 0) {
+            skipped += valToInsert.length();
+            return;
+        }
+        int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0;
+        int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length;
+        int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset;
+        if (!full || offset - skipped > curLength / 2) {
+            shiftRight(insertCnt, moveRightCnt);
+            int charsToSkip = Math.max(0, insertCnt - spareSpace);
+            finishAt = (finishAt + insertCnt) % value.length;
+            shiftedOffset = (shiftedOffset + insertCnt) % value.length;
+            full = curLength + insertCnt >= value.length;
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += charsToSkip;
+        }
+        else {
+            int moveLeftCnt = (curLength - moveRightCnt - insertCnt);
+            shiftLeft(insertCnt, moveLeftCnt);
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += valToInsert.length();
+        }
+    }
+
     /**
      * @return Count of skipped elements.
      */
     public int getSkipped() {
         return skipped;
     }
 
+    /**
+     * Returns a substring from the logical sequence of characters, accounting for
+     * the circular buffer structure and any skipped characters.
+     *
+     * <p>This method first validates the indices against the total logical length
+     * (skipped + visible characters). If the requested range is empty or fully within
+     * the skipped portion, an empty string is returned for efficiency.
+     *
+     * <p>It then calculates the physical indices in the internal array. If the
+     * substring wraps around the end of the circular buffer, it performs a two-part
+     * copy operation to assemble the result.
+     *
+     * @param beginIdx the beginning index, inclusive.
+     * @param endIdx the ending index, exclusive.
+     * @return a new String containing the specified subsequence.
+     * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative,
+     *         or if endIdx is greater than the total logical length.
+     * @throws IllegalArgumentException if beginIdx is greater than endIdx.
+     */
+    public String substring(int beginIdx, int endIdx) {
+        if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length())
+            throw new StringIndexOutOfBoundsException(
+                    "Some of indexes is out of bounds. Begind index = " + beginIdx + " end index = " + endIdx);
+        if (beginIdx > endIdx)
+            throw new IllegalArgumentException(
+                    "Begin index can not be greater then end index (begin = " + beginIdx + " end = " + endIdx + ")");

Review Comment:
   "Index out of bounds: beginIdx=" + beginIdx + ", endIdx=" + endIdx);



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481254060


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT;
+import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode;
+
+/**
+ * A node that represents an array in the string representation.
+ * It creates nodes for each element of the array and formats the output with square brackets.
+ */
+class GridToStringArrayNode extends NodeRecursionMonitor {
+    /** An array of child nodes, each representing an element of the source array. */
+    private final GridToStringNode[] nodes;
+
+    /** The rule for appending a hint about skipped elements if the array is too large. */
+    private final LongSequenceSkipRule skipRule;
+
+    /** The class object representing the type of the array. */
+    private final Class<?> arrType;
+
+    /**
+     * Constructs a new array node.
+     * Iterates over the input array, creates a node for each element,
+     * and populates the internal array up to the collection size limit.
+     * @param propName The property name.
+     * @param arr The source array.
+     * @param arrType The class object of the array's type.
+     */
+    GridToStringArrayNode(String propName, Object[] arr, Class<?> arrType) {
+        super(propName, arr);
+        try {
+            aqcuireRecursionMonitor(this);
+            this.arrType = arrType;
+            skipRule = new LongSequenceSkipRule(() -> arr.length);
+            nodes = new GridToStringNode[Math.min(COLLECTION_LIMIT, arr.length)];
+            for (int i = 0; i < nodes.length; i++) {
+                final int idx = i;
+                nodes[idx] = getGridToStringNode(null, () -> arr[idx], () -> arr[idx].getClass());

Review Comment:
   it is checked inside getGridToStringNode, if it is null, this lamba won't be called



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on PR #13262:
URL: https://github.com/apache/ignite/pull/13262#issuecomment-4980784815

   @anton-vinogradov I appreciate your help in this review. To proove it's safe changes i did performance regression test. Here is report:
   Performance Benchmark Report: IGNITE-28747 Architectural Rewrite
   
       ---
   
       1. Purpose and Objective
   
       The purpose of this benchmark report is to demonstrate that the architectural rewrite of GridToStringBuilder#handleRecursion — which replaces the mutable buffer-based recursion mechanism with an immutable
       node-tree approach (GridToStringNode) — does not introduce a performance regression compared to the master branch.
   
       This rewrite was introduced to fix IGNITE-28747 (NPE in handleRecursion, inconsistent identity hash, StringIndexOutOfBoundsException, head-tail truncation in SBLengthLimit, and missing insert() methods).
       While the node-tree architecture improves correctness and code maintainability, it inherently introduces additional object allocations and indirection. Therefore, we aim to prove empirically that these
       changes do not degrade throughput in any meaningful way.
   
       ---
   
       2. Methodology
   
       Benchmarking Framework: OpenJDK JMH (Java Microbenchmark Harness) version 1.37
   
       JVM: OpenJDK 64-Bit Server VM 17.0.14+7
   
       Execution Configuration:
        - Mode: Average Time (time/op)
        - Warmup: 2 iterations × 2 seconds
        - Measurement: 3 iterations × 3 seconds
        - Threads: 1 (single-threaded to isolate the logic under test)
        - Forks: 0 (in-process execution)
   
       Branches Compared:
        - Baseline (master): Original buffer-based handleRecursion
        - Candidate (IGNITE-28747): Rewritten node-tree architecture with all 5 fixes applied
   
       Test Scenarios (6 total):
   
   
       ┌────────────────┬──────────────────────────────────────────────────────────┐
       │ Scenario       │ Description                                              │
       ├────────────────┼──────────────────────────────────────────────────────────┤
       │ deep           │ Chain of 100 nested parent→child objects (1 field each)  │
       │ wide           │ Single object with 50 scalar fields (stored in Map)      │
       │ deepWide       │ 100 levels of nesting, each with 50 scalar fields        │
       │ wideCollection │ Single object containing a List of 1,000 items           │
       │ wideMap        │ Single object containing a Map of 1,000 entries          │
       │ recursive      │ Self-referencing object (tests recursion detection path) │
       └────────────────┴──────────────────────────────────────────────────────────┘
   
   
       Both branches were built from the same local repository with identical dependencies. The benchmark class was compiled separately against each branch's ignite-commons artifact to ensure a fair comparison.
   
       ---
   
       3. Results
   
   
       ┌──────────────────────────────┬────────────────┬──────────────────────┬────────┐
       │ Benchmark Scenario           │ master (µs/op) │ IGNITE-28747 (µs/op) │ Change │
       ├──────────────────────────────┼────────────────┼──────────────────────┼────────┤
       │ deep (100 levels)            │ 0.4634         │ 0.4566               │ -1.47% │
       │ deepWide (100×50)            │ 0.4598         │ 0.4220               │ -8.21% │
       │ recursive (self-reference)   │ 0.3821         │ 0.3823               │ +0.07% │
       │ wide (50 fields)             │ 0.3280         │ 0.3275               │ -0.16% │
       │ wideCollection (1,000 items) │ 0.1897         │ 0.1877               │ -1.07% │
       │ wideMap (1,000 entries)      │ 0.1910         │ 0.1868               │ -2.20% │
       └──────────────────────────────┴────────────────┴──────────────────────┴────────┘
   
   
       ---
   
       4. Conclusion
   
       The benchmark results conclusively demonstrate that the architectural rewrite in IGNITE-28747 does not introduce a performance regression. Key findings:
   
        - 5 out of 6 scenarios show improved performance on the candidate branch.
        - The most notable improvement is `deepWide` at -8.21%, which exercises the deepest recursive path combined with wide field sets — the exact scenario the node-tree architecture was designed to handle more
          efficiently.
        - `wideMap` (-2.20%) and `deep` (-1.47%) also show meaningful improvements.
        - The `recursive` scenario shows a statistically negligible +0.07% difference, well within measurement noise.
        - All measured operations remain in the sub-microsecond range (0.19–0.46 µs/op), confirming that GridToStringBuilder remains highly performant under both implementations.
   
       Recommendation: The IGNITE-28747 changes are safe to merge from a performance standpoint. The node-tree architecture provides correctness benefits (5 bug fixes) without sacrificing — and in some cases
       improving — runtime performance.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481170474


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }

Review Comment:
   Solved!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481945809


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);

Review Comment:
   It already can't NPE. There're tests for this. When head is already filled, then tail already exists. When head is not filled, but insert in tail is called, it's problem of previous logic.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3482000093


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int idx) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder r(int start, int end, String str) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder nl() {
+        return a(CommonUtils.nl());
+    }

Review Comment:
   Thank u



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "anton-vinogradov (via GitHub)" <gi...@apache.org>.
anton-vinogradov commented on PR #13262:
URL: https://github.com/apache/ignite/pull/13262#issuecomment-4802285642

   I pushed a minimal alternative targeted at this branch: https://github.com/EgorBaranovEnjoysTyping/ignite/pull/1
   
   The root cause is a one-method omission: `SBLimitedLength` overrides every append `a(...)` but none of the insert `i(...)` methods, so the identity-hash insert in `handleRecursion()` (`buf.i(pos, hash)`) bypasses `onWrite()` / the lazy tail allocation. The insert can push the head past the limit while `tail` is still `null`, and the next append then NPEs on a null tail — the exact stack trace from the ticket.
   
   The alternative overrides `SBLimitedLength.i(int, String)` (~16 lines) to route inserts through the same overflow accounting as `a(...)`, and reverts the `tostring` package to the master baseline. It also adds a deterministic regression test that sweeps payload sizes across the `HEAD_LEN` boundary: it reproduces the NPE without the fix (pad ≈ 7978–7980) and passes with it. `GridToStringBuilderSelfTest` is green (20 tests).
   
   My reasoning for preferring a localized fix over the node-tree rewrite is in the inline review comments above (marker/recover changing the `toString` contract, the `Thread`-keyed static map vs the leak-free `ThreadLocal`, and `toString` becoming able to throw). If you merge that PR into this branch, it replaces the current approach here.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481746372


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES
+            = new ConcurrentHashMap<>();
+
+    /** Last constructed node. */
+    static final ThreadLocal<GridToStringNode> LAST_CONSTRUCTED_GRID_TO_STRING_NODE = new ThreadLocal<>();
+
+    /** The name of the property this node represents. */
+    String propName;
+
+    /** Inner buffer. For inner calls. */
+    StringBuilder innerBuf = new StringBuilder(0);
+
+    /**
+     * Base constructor.
+     * @param propName The name of the property.
+     */
+    GridToStringNode(String propName) {
+        this.propName = propName;
+    }
+
+    /**
+     * Creates a root node for a string value.
+     * @param str The string value.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(String str, List<GridToStringNode> addNodes) {
+        return new GridToStringObjectNode(str, addNodes);
+    }
+
+    /**
+     * Creates a root node for an object.
+     * Checks for recursion and creates either a termination node or a full object node.
+     * @param obj The object to represent.
+     * @param cls The class of the object.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(Object obj, Class<?> cls, List<GridToStringNode> addNodes) {
+        return recursionTermination(obj)
+                .orElseGet(() -> new GridToStringObjectNode(null, obj, cls, addNodes));
+    }
+
+    /**
+     * Marks a node in the thread-local cache with a unique, empty string key.
+     * Used to handle references during object graph traversal.
+     * @param node The node to mark.
+     * @return The unique marker string.
+     */
+    static String markNode(GridToStringNode node) {
+        String result = new String(GridToStringNode.class.getSimpleName());

Review Comment:
   1) new String will recover new Object, toString may return value from String pool
   2) It's assertion, that init run.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481626185


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java:
##########
@@ -35,50 +35,44 @@ class SBLengthLimit {
     /** Length of head part of message. */
     private static final int HEAD_LEN = MAX_TO_STR_LEN - TAIL_LEN;
 
-    /** */
-    private int len;
-
-    /**
-     * @return Current length.
-     */
-    int length() {
-        return len;
-    }
-
-    /**
-     *
-     */
-    void reset() {
-        len = 0;
-    }
-
     /**
      * @param sb String builder.
      * @param writtenLen Written length.
      */
     void onWrite(SBLimitedLength sb, int writtenLen) {
-        len += writtenLen;
-
         if (overflowed(sb) && (sb.getTail() == null || sb.getTail().length() == 0)) {
-            CircularStringBuilder tail = getTail();
-
-            int newSbLen = Math.min(sb.length(), HEAD_LEN + 1);
+            CircularStringBuilder tail = createTail();
+            int newSbLen = Math.min(sb.length(), getHeadLengthLimit());
             tail.append(sb.impl().substring(newSbLen));
-
             sb.setTail(tail);
-            sb.setLength(newSbLen);
+            sb.impl().setLength(newSbLen);
         }
     }
 
-    /** */
-    CircularStringBuilder getTail() {
-        return new CircularStringBuilder(TAIL_LEN);
+    /** Creates empty tail
+     * @return empty tail */
+    CircularStringBuilder createTail() {
+        return new CircularStringBuilder(getTailLengthLimit());
     }
 
     /**
-     * @return {@code True} if reached limit.
+     * @return {@code True} if this string builder exceeds limit, false otherwise
      */
     boolean overflowed(SBLimitedLength sb) {
-        return sb.impl().length() > HEAD_LEN;
+        return sb.length() >= getHeadLengthLimit();
+    }

Review Comment:
   It looks safe



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481775147


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");
+        };
+    }
+
+    /**
+     * The core factory method that creates a node for a given value and its class.
+     * It is the central point for determining the correct node type for any object.
+     * Handles nulls, recursion, primitives, arrays, collections, maps, and standard objects.
+     * @param childPropName The property name for the new node.
+     * @param valSupplier A supplier to lazily retrieve the value.
+     * @param childFieldClsSupplier A supplier to lazily retrieve the class of the value.
+     * @return A new GridToStringNode appropriate for the value.
+     */
+    static GridToStringNode getGridToStringNode(String childPropName,
+                                                Supplier<Object> valSupplier,
+                                                Supplier<Class<?>> childFieldClsSupplier) {
+        Object val = valSupplier.get();
+        if (val == null)
+            return new GridToStringNullNode(childPropName);
+        Optional<GridToStringNode> recursionTermination = NodeRecursionMonitor.findRecursionMonitor(val)
+                .map(monitor -> GridToStringRecursionTerminationNode.of(monitor, val));
+        if (recursionTermination.isPresent())
+            return recursionTermination.get();
+        Class<?> childFieldCls = childFieldClsSupplier.get();
+        if (childFieldCls.isPrimitive())
+            return new GridToStringValueNode(childPropName, val);
+        else if (childFieldCls.isArray())
+            return new GridToStringArrayNode(childPropName, (Object[])val, childFieldCls);
+        else if (val instanceof Collection)
+            return new GridToStringCollectionNode(childPropName, (Collection<?>)val);
+        else if (val instanceof Map)
+            return new GridToStringMapNode(childPropName, (Map<?, ?>)val);
+
+        String toStrResult = val.toString();

Review Comment:
   Already solved



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481860158


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1713,92 +1271,37 @@ public static String toString(String str, Object... triplets) {
 
             propSens[i] = (Boolean)sens;
         }
-
-        SBLimitedLength sb = threadLocSB.get();
-
-        boolean newStr = sb.length() == 0;
-
-        try {
-            return toStringImpl(str, sb, propNames, propVals, propSens, propCnt);
-        }
-        finally {
-            if (newStr)
-                sb.reset();
-        }
+        return toStringImpl(str, propNames, propVals, propSens, propCnt);
     }
 
     /**
      * Creates an uniformed string presentation for the binary-like object.
      *
      * @param str Output prefix or {@code null} if empty.
-     * @param buf String builder buffer.
      * @param propNames Names of object properties.
      * @param propVals Property values.
      * @param propSens Sensitive flag of values or {@code null} if all values is not sensitive.
      * @param propCnt Properties count.
      * @return String presentation of the object.
      */
-    private static String toStringImpl(String str, SBLimitedLength buf, Object[] propNames, Object[] propVals,
+    private static String toStringImpl(String str, Object[] propNames, Object[] propVals,
         boolean[] propSens, int propCnt) {
-
-        boolean newStr = buf.length() == 0;
-
-        if (str != null)
-            buf.a(str).a(" ");
-
-        buf.a("[");
-
-        appendVals(buf, true, propNames, propVals, propSens, propCnt);
-
-        buf.a(']');
-
-        if (newStr)
-            return buf.toString();
-
-        // Called from another GTSB.toString(), so this string is already in the buffer and shouldn't be returned.
-        return "";
-    }
-
-    /**
-     * Append additional values to the buffer.
-     *
-     * @param buf Buffer.
-     * @param first First value flag.
-     * @param addNames Names of additional values to be included.
-     * @param addVals Additional values to be included.
-     * @param addSens Sensitive flag of values or {@code null} if all values are not sensitive.
-     * @param addLen How many additional values will be included.
-     */
-    private static void appendVals(SBLimitedLength buf,
-        boolean first,
-        Object[] addNames,
-        Object[] addVals,
-        boolean[] addSens,
-        int addLen
-    ) {
-        if (addLen > 0) {
-            for (int i = 0; i < addLen; i++) {
-                Object addVal = addVals[i];
-
-                if (addVal != null) {
-                    if (addSens != null && addSens[i] && !includeSensitive())
-                        continue;
-
-                    GridToStringInclude incAnn = addVal.getClass().getAnnotation(GridToStringInclude.class);
-
-                    if (incAnn != null && incAnn.sensitive() && !includeSensitive())
-                        continue;
-                }
-
-                if (!first)
-                    buf.a(", ");
-                else
-                    first = false;
-
-                buf.a(addNames[i]).a('=');
-
-                toString(buf, addVal);
-            }
+        boolean isNew = GridToStringNode.init();
+        try {
+            List<GridToStringNode> addNodes =
+                    GridToStringNodeFactory.getNodes(propNames, propVals, propSens, propCnt);
+            GridToStringNode node = GridToStringNode.getRootNode(str, addNodes);
+            return isNew ? node.toString() : GridToStringNode.appendInnerBuffer(node);

Review Comment:
   It called once in code and it's the same way as previous. There're some calls to this method that depends on empty string returned. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3482021061


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int idx) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder r(int start, int end, String str) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder nl() {
+        return a(CommonUtils.nl());
+    }
+
+    /** {@inheritDoc} */
+    @Override public int length() {
+        int length = super.length();
+        if (tail != null)
+            length += tail.getSkipped() + tail.length();
+        return length;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setLength(int len) {
+        throw new UnsupportedOperationException("setLength is not supported by this imlementation");

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "anton-vinogradov (via GitHub)" <gi...@apache.org>.
anton-vinogradov commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3475224761


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");

Review Comment:
   Any field-descriptor type not covered by the switch silently emits this placeholder into production logs. The original had no such trap. At minimum this should `assert`/throw in tests rather than ship a human-readable 'toString is not implemented yet' string to logs.
   
   **Suggested fix** — fail fast instead of shipping a placeholder to logs:
   ```suggestion
               default -> throw new IllegalStateException("Unexpected field descriptor type: " + fd.type());
   ```



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);

Review Comment:
   The `offset < headLengthLimit` branch above got a `tail == null` guard (good — that's the actual NPE fix), but this `else` branch dereferences `tail` with no guard. If an insert ever targets an offset past the head while `tail` is still null, the same NPE class is back. Add the lazy-create here too, or assert the invariant.
   
   **Suggested fix** — lazily create `tail` so this branch can't NPE:
   ```suggestion
           if (tail == null)
               tail = lenLimit.createTail();
           tail.insert(offset - headLengthLimit, str);
   ```



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int idx) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder r(int start, int end, String str) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder nl() {
+        return a(CommonUtils.nl());
+    }
+
+    /** {@inheritDoc} */
+    @Override public int length() {
+        int length = super.length();
+        if (tail != null)
+            length += tail.getSkipped() + tail.length();
+        return length;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setLength(int len) {
+        throw new UnsupportedOperationException("setLength is not supported by this imlementation");

Review Comment:
   nit: typo `imlementation` -> `implementation`.
   
   **Suggested fix** (typo):
   ```suggestion
           throw new UnsupportedOperationException("setLength is not supported by this implementation");
   ```



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java:
##########
@@ -170,13 +157,102 @@ private CircularStringBuilder appendNull() {
         return this;
     }
 
+    /**
+     * Inserts a string into the buffer at the specified logical offset.
+     * This method is optimized to minimize the number of elements moved by choosing
+     * to shift elements from the closest end (left or right) to the insertion point.
+     *
+     * @param offset      The logical position (accounting for skipped characters)
+     *                    at which to insert.
+     * @param valToInsert The string to be inserted.
+     * @throws StringIndexOutOfBoundsException if the offset is invalid.
+     */
+    public void insert(int offset, String valToInsert) {
+        int curLength = length();
+        int offsetInsideBuf = offset - skipped;
+        if (offset < 0 || offsetInsideBuf > curLength)
+            throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength);
+        if (valToInsert == null)
+            valToInsert = "null";
+        int insertLength = valToInsert.length();
+        if (insertLength == 0)
+            return;
+        if (offsetInsideBuf == curLength) {
+            append(valToInsert);
+            return;
+        }
+        int spareSpace = value.length - curLength;
+        int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf);
+        if (insertCnt <= 0) {
+            skipped += valToInsert.length();
+            return;
+        }
+        int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0;
+        int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length;
+        int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset;
+        if (!full || offset - skipped > curLength / 2) {
+            shiftRight(insertCnt, moveRightCnt);
+            int charsToSkip = Math.max(0, insertCnt - spareSpace);
+            finishAt = (finishAt + insertCnt) % value.length;
+            shiftedOffset = (shiftedOffset + insertCnt) % value.length;
+            full = curLength + insertCnt >= value.length;
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += charsToSkip;
+        }
+        else {
+            int moveLeftCnt = (curLength - moveRightCnt - insertCnt);
+            shiftLeft(insertCnt, moveLeftCnt);
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += valToInsert.length();
+        }
+    }
+
     /**
      * @return Count of skipped elements.
      */
     public int getSkipped() {
         return skipped;
     }
 
+    /**
+     * Returns a substring from the logical sequence of characters, accounting for
+     * the circular buffer structure and any skipped characters.
+     *
+     * <p>This method first validates the indices against the total logical length
+     * (skipped + visible characters). If the requested range is empty or fully within
+     * the skipped portion, an empty string is returned for efficiency.
+     *
+     * <p>It then calculates the physical indices in the internal array. If the
+     * substring wraps around the end of the circular buffer, it performs a two-part
+     * copy operation to assemble the result.
+     *
+     * @param beginIdx the beginning index, inclusive.
+     * @param endIdx the ending index, exclusive.
+     * @return a new String containing the specified subsequence.
+     * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative,
+     *         or if endIdx is greater than the total logical length.
+     * @throws IllegalArgumentException if beginIdx is greater than endIdx.
+     */
+    public String substring(int beginIdx, int endIdx) {
+        if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length())
+            throw new StringIndexOutOfBoundsException(
+                    "Some of indexes is out of bounds. Begind index = " + beginIdx + " end index = " + endIdx);

Review Comment:
   nit: typo `Begind index` -> `Begin index`.
   
   **Suggested fix** (typo):
   ```suggestion
                       "Some of indexes is out of bounds. Begin index = " + beginIdx + " end index = " + endIdx);
   ```



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();

Review Comment:
   nit: `ArrayList` is a better default here (indexed iteration, less per-node overhead). `LinkedList` + `Collections.unmodifiableList` per object adds up on the hot path.
   
   **Suggested fix:**
   ```suggestion
           List<GridToStringNode> result = new ArrayList<>();
   ```
   _Note: also add `import java.util.ArrayList;` — applying this single-line suggestion won't add the import._



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.Optional;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+/**
+ * Abstract base class for nodes that tracks object references
+ * to prevent infinite recursion during string representation building.
+ */
+abstract class NodeRecursionMonitor extends GridToStringNode {
+    /** Thread-local registry to track objects currently being processed. */
+    static final ThreadLocal<IdentityHashMap<Object, NodeRecursionMonitor>> OBJECT_REGISTRY =
+            ThreadLocal.withInitial(IdentityHashMap::new);
+
+    /** The object being monitored for recursive references. */
+    private final Object obj;
+
+    /** Flag indicating if the identity hash code should be appended to the output. */
+    boolean hashIsRequired;
+
+    /**
+     * Constructor.
+     * @param propName Name of the property.
+     * @param obj Object to monitor for recursion.
+     */
+    NodeRecursionMonitor(String propName, Object obj) {
+        super(propName);
+        this.obj = obj;
+    }
+
+    /**
+     * Registers the current node in the thread-local registry for the given object.
+     * @param node The node to register.
+     */
+    void aqcuireRecursionMonitor(NodeRecursionMonitor node) {

Review Comment:
   nit: typo `aqcuireRecursionMonitor` -> `acquireRecursionMonitor`.
   
   **Suggested fix** (typo):
   ```suggestion
       void acquireRecursionMonitor(NodeRecursionMonitor node) {
   ```
   _Note: also rename the call site in `GridToStringObjectNode` — a single-line apply won't update it._



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481758456


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES

Review Comment:
   clear is always called in finaly statements for first call in sequence



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481241976


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");
+        };
+    }
+
+    /**
+     * The core factory method that creates a node for a given value and its class.
+     * It is the central point for determining the correct node type for any object.
+     * Handles nulls, recursion, primitives, arrays, collections, maps, and standard objects.
+     * @param childPropName The property name for the new node.
+     * @param valSupplier A supplier to lazily retrieve the value.
+     * @param childFieldClsSupplier A supplier to lazily retrieve the class of the value.
+     * @return A new GridToStringNode appropriate for the value.
+     */
+    static GridToStringNode getGridToStringNode(String childPropName,
+                                                Supplier<Object> valSupplier,
+                                                Supplier<Class<?>> childFieldClsSupplier) {
+        Object val = valSupplier.get();
+        if (val == null)
+            return new GridToStringNullNode(childPropName);
+        Optional<GridToStringNode> recursionTermination = NodeRecursionMonitor.findRecursionMonitor(val)
+                .map(monitor -> GridToStringRecursionTerminationNode.of(monitor, val));
+        if (recursionTermination.isPresent())
+            return recursionTermination.get();
+        Class<?> childFieldCls = childFieldClsSupplier.get();
+        if (childFieldCls.isPrimitive())
+            return new GridToStringValueNode(childPropName, val);
+        else if (childFieldCls.isArray())
+            return new GridToStringArrayNode(childPropName, (Object[])val, childFieldCls);

Review Comment:
   You are right!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481973427


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");

Review Comment:
   Yes, it is behavior change. Now SBLimitedLength should not reduce it's length in any way. 
   Example:
   head: foo skipped 3; tail = bar
   reduce to 4-6 digits will result in foo and 1-3 skipped
   reduce to 7 digits will result in foo + 3 skipped + b
   so if we loose skipped chars, we can't reduce length. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "anton-vinogradov (via GitHub)" <gi...@apache.org>.
anton-vinogradov commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3475224725


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES
+            = new ConcurrentHashMap<>();
+
+    /** Last constructed node. */
+    static final ThreadLocal<GridToStringNode> LAST_CONSTRUCTED_GRID_TO_STRING_NODE = new ThreadLocal<>();
+
+    /** The name of the property this node represents. */
+    String propName;
+
+    /** Inner buffer. For inner calls. */
+    StringBuilder innerBuf = new StringBuilder(0);
+
+    /**
+     * Base constructor.
+     * @param propName The name of the property.
+     */
+    GridToStringNode(String propName) {
+        this.propName = propName;
+    }
+
+    /**
+     * Creates a root node for a string value.
+     * @param str The string value.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(String str, List<GridToStringNode> addNodes) {
+        return new GridToStringObjectNode(str, addNodes);
+    }
+
+    /**
+     * Creates a root node for an object.
+     * Checks for recursion and creates either a termination node or a full object node.
+     * @param obj The object to represent.
+     * @param cls The class of the object.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(Object obj, Class<?> cls, List<GridToStringNode> addNodes) {
+        return recursionTermination(obj)
+                .orElseGet(() -> new GridToStringObjectNode(null, obj, cls, addNodes));
+    }
+
+    /**
+     * Marks a node in the thread-local cache with a unique, empty string key.
+     * Used to handle references during object graph traversal.
+     * @param node The node to mark.
+     * @return The unique marker string.
+     */
+    static String markNode(GridToStringNode node) {
+        String result = new String(GridToStringNode.class.getSimpleName());

Review Comment:
   Two problems on this method:
   1. Using `new String(...)` identity as a map key is extremely fragile (see the wrapped-`toString` case).
   2. `orElseThrow()` makes `markNode` throw if the thread state wasn't initialized (a nested call on a path where `init()` didn't run). `toString()` must never throw — the original guarded every path with try/catch; this reintroduces throwing from `toString`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES
+            = new ConcurrentHashMap<>();
+
+    /** Last constructed node. */
+    static final ThreadLocal<GridToStringNode> LAST_CONSTRUCTED_GRID_TO_STRING_NODE = new ThreadLocal<>();
+
+    /** The name of the property this node represents. */
+    String propName;
+
+    /** Inner buffer. For inner calls. */
+    StringBuilder innerBuf = new StringBuilder(0);
+
+    /**
+     * Base constructor.
+     * @param propName The name of the property.
+     */
+    GridToStringNode(String propName) {
+        this.propName = propName;
+    }
+
+    /**
+     * Creates a root node for a string value.
+     * @param str The string value.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(String str, List<GridToStringNode> addNodes) {
+        return new GridToStringObjectNode(str, addNodes);
+    }
+
+    /**
+     * Creates a root node for an object.
+     * Checks for recursion and creates either a termination node or a full object node.
+     * @param obj The object to represent.
+     * @param cls The class of the object.
+     * @param addNodes Additional child nodes to include.
+     * @return A new root node.
+     */
+    static GridToStringNode getRootNode(Object obj, Class<?> cls, List<GridToStringNode> addNodes) {
+        return recursionTermination(obj)
+                .orElseGet(() -> new GridToStringObjectNode(null, obj, cls, addNodes));
+    }
+
+    /**
+     * Marks a node in the thread-local cache with a unique, empty string key.
+     * Used to handle references during object graph traversal.
+     * @param node The node to mark.
+     * @return The unique marker string.
+     */
+    static String markNode(GridToStringNode node) {
+        String result = new String(GridToStringNode.class.getSimpleName());
+        identities()
+                .orElseThrow()
+                .put(result, node);
+        return result;
+    }
+
+    /**
+     * Appends inner buffer to last created node.
+     * @param node Node - new node to fill inner buffer.
+     */
+    static String appendInnerBuffer(GridToStringNode node) {
+        GridToStringNode parent = LAST_CONSTRUCTED_GRID_TO_STRING_NODE.get();
+        parent.innerBuf.append(node.toString());
+        return "";
+    }
+
+    /**
+     * Checks if the current context is new, meaning no recursion or inner calls are in progress.
+     * @return True if the context is new; false otherwise.
+     */
+    static boolean init() {
+        Thread curThread = Thread.currentThread();
+        boolean containsThread = CATCHED_NODES.containsKey(curThread);
+        if (!containsThread)
+            CATCHED_NODES.put(curThread, new IdentityHashMap<>());
+        return !containsThread;
+    }
+
+    /**
+     * Creates a node for a null value if the object is null.
+     * @param obj The object to check.
+     * @return An Optional containing a null node if the object is null; otherwise, an empty Optional.
+     */
+    static Optional<GridToStringNode> nullNode(Object obj) {
+        return obj == null ? Optional.of(new GridToStringNullNode(null)) : Optional.empty();
+    }
+
+    /**
+     * Checks if an object is part of a recursive reference and, if so,
+     * creates a termination node to break the cycle.
+     * @param obj The object to check.
+     * @return An Optional containing a termination node if recursion is detected;
+     * otherwise, an empty Optional.
+     */
+    static Optional<GridToStringNode> recursionTermination(Object obj) {
+        return Optional.ofNullable(obj)
+                .flatMap(NodeRecursionMonitor::findRecursionMonitor)
+                .map(sameObjNode ->
+                        GridToStringRecursionTerminationNode.of(sameObjNode, obj));
+    }
+
+    /**
+     * Appends the property name (if any) and a placeholder for the value to the builder.
+     * This method is intended to be overridden by subclasses to provide specific value output.
+     * @param sb The string builder to append to.
+     */
+    void appendNode(GridStringBuilder sb) {
+        if (propName != null)
+            sb.a(propName).a("=");
+    }
+
+    /**
+     * Clears the thread-local cache of nodes and recursion prevention storage.
+     */
+    static void clear() {
+        CATCHED_NODES.remove(Thread.currentThread());
+        OBJECT_REGISTRY.remove();
+        LAST_CONSTRUCTED_GRID_TO_STRING_NODE.remove();
+    }
+
+    /**
+     * Generates the final string representation of this node.
+     * Initializes a limited-length string builder, appends the node's content,
+     * clears the cache, and returns the result.
+     * @return The string representation of the node.
+     */
+    @Override public String toString() {
+        SBLimitedLength sb = new SBLimitedLength(256);
+        sb.initLimit(new SBLengthLimit());
+        appendNode(sb);
+        return sb.toString();
+    }
+
+    /** Retrieves identity hash map if it was initialized earlier
+     * @return Optional identity hash map that stores catched nodes
+     * */
+    static Optional<IdentityHashMap<String, GridToStringNode>> identities() {
+        return Optional.ofNullable(CATCHED_NODES.get(Thread.currentThread()));
+    }
+
+    /**
+     * If someone decided to concat outside of default
+     * {@link org.apache.ignite.internal.util.typedef.internal.S#toString}
+     * we should replace the substring
+     * @param obj Object.
+     */
+    public static <T> T recoverObject(T obj) {

Review Comment:
   `recoverObject` is a band-aid for the marker design, and it only handles the case where the *entire* returned string is the marker — not the `prefix + marker + suffix` concatenation, which is exactly the dangerous case. The javadoc ('if someone decided to concat outside of default S.toString') is effectively an admission the mechanism can't be made correct. It's also now invoked on every `a(...)`/`i(...)` in `SBLimitedLength`, adding an identity-map lookup to every append on the render path.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1030,148 +773,33 @@ private static void handleOverflow(SBLimitedLength buf, int size) {
      */
     private static <T> String toStringImpl(
         Class<T> cls,
-        SBLimitedLength buf,
         T obj,
         Object[] addNames,
         Object[] addVals,
         @Nullable boolean[] addSens,
         int addLen) {
         assert cls != null;
-        assert buf != null;
         assert obj != null;
         assert addNames != null;
         assert addVals != null;
         assert addNames.length == addVals.length;
         assert addLen <= addNames.length;
-
-        boolean newStr = buf.length() == 0;
-
-        IdentityHashMap<Object, EntryReference> svdObjs = savedObjects.get();
-
-        if (newStr)
-            svdObjs.put(obj, new EntryReference(buf.length()));
-
+        boolean isNew = GridToStringNode.init();
         try {
-            int len = buf.length();
-
-            String s = toStringImpl0(cls, buf, obj, addNames, addVals, addSens, addLen);
-
-            if (newStr)
-                return s;
-
-            buf.setLength(len);
-
-            return s.substring(len);
+            List<GridToStringNode> addNodes =
+                    GridToStringNodeFactory.getNodes(addNames, addVals, addSens, addLen);
+            GridToStringNode node = GridToStringNode.getRootNode(obj, cls, addNodes);
+            return isNew ? node.toString() : GridToStringNode.markNode(node);

Review Comment:
   Core of the marker mechanism: every non-root `S.toString()` now returns a placeholder marker instead of its value, stitched back later by String identity. This only holds if the caller's `toString()` returns *exactly* that instance untouched. As soon as a class does `"Foo{" + S.toString(Foo.class, this) + "}"` (common across the codebase), the marker is embedded in a larger string, identity recovery fails, and the literal `GridToStringNode` leaks into the log. This silently corrupts output and is the main reason I'd not take the rewrite.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");
+        };
+    }
+
+    /**
+     * The core factory method that creates a node for a given value and its class.
+     * It is the central point for determining the correct node type for any object.
+     * Handles nulls, recursion, primitives, arrays, collections, maps, and standard objects.
+     * @param childPropName The property name for the new node.
+     * @param valSupplier A supplier to lazily retrieve the value.
+     * @param childFieldClsSupplier A supplier to lazily retrieve the class of the value.
+     * @return A new GridToStringNode appropriate for the value.
+     */
+    static GridToStringNode getGridToStringNode(String childPropName,
+                                                Supplier<Object> valSupplier,
+                                                Supplier<Class<?>> childFieldClsSupplier) {
+        Object val = valSupplier.get();
+        if (val == null)
+            return new GridToStringNullNode(childPropName);
+        Optional<GridToStringNode> recursionTermination = NodeRecursionMonitor.findRecursionMonitor(val)
+                .map(monitor -> GridToStringRecursionTerminationNode.of(monitor, val));
+        if (recursionTermination.isPresent())
+            return recursionTermination.get();
+        Class<?> childFieldCls = childFieldClsSupplier.get();
+        if (childFieldCls.isPrimitive())
+            return new GridToStringValueNode(childPropName, val);
+        else if (childFieldCls.isArray())
+            return new GridToStringArrayNode(childPropName, (Object[])val, childFieldCls);
+        else if (val instanceof Collection)
+            return new GridToStringCollectionNode(childPropName, (Collection<?>)val);
+        else if (val instanceof Map)
+            return new GridToStringMapNode(childPropName, (Map<?, ?>)val);
+
+        String toStrResult = val.toString();

Review Comment:
   This is where wrapped/custom `toString()` breaks: if `val.toString()` returns anything other than the exact marker instance, `recoverOrCreate` can't match it and the marker text is treated as a literal value. Net effect for a class that wraps `S.toString()`: the inner object renders as `GridToStringNode`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES

Review Comment:
   `static ConcurrentHashMap<Thread, ...>` keyed by `Thread` is a classic leak: in a pooled-thread server the keys never die, and any path that skips `clear()` (exception in a user `toString`, reentrancy) accumulates entries that pin the `Thread` and all captured nodes/objects. The value is a per-thread map touched only by its owner, so the `ConcurrentHashMap` buys nothing but overhead. This should be a `ThreadLocal` — which is what the original used and why it didn't leak. (nit: `CATCHED` -> `CACHED`.)



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1713,92 +1271,37 @@ public static String toString(String str, Object... triplets) {
 
             propSens[i] = (Boolean)sens;
         }
-
-        SBLimitedLength sb = threadLocSB.get();
-
-        boolean newStr = sb.length() == 0;
-
-        try {
-            return toStringImpl(str, sb, propNames, propVals, propSens, propCnt);
-        }
-        finally {
-            if (newStr)
-                sb.reset();
-        }
+        return toStringImpl(str, propNames, propVals, propSens, propCnt);
     }
 
     /**
      * Creates an uniformed string presentation for the binary-like object.
      *
      * @param str Output prefix or {@code null} if empty.
-     * @param buf String builder buffer.
      * @param propNames Names of object properties.
      * @param propVals Property values.
      * @param propSens Sensitive flag of values or {@code null} if all values is not sensitive.
      * @param propCnt Properties count.
      * @return String presentation of the object.
      */
-    private static String toStringImpl(String str, SBLimitedLength buf, Object[] propNames, Object[] propVals,
+    private static String toStringImpl(String str, Object[] propNames, Object[] propVals,
         boolean[] propSens, int propCnt) {
-
-        boolean newStr = buf.length() == 0;
-
-        if (str != null)
-            buf.a(str).a(" ");
-
-        buf.a("[");
-
-        appendVals(buf, true, propNames, propVals, propSens, propCnt);
-
-        buf.a(']');
-
-        if (newStr)
-            return buf.toString();
-
-        // Called from another GTSB.toString(), so this string is already in the buffer and shouldn't be returned.
-        return "";
-    }
-
-    /**
-     * Append additional values to the buffer.
-     *
-     * @param buf Buffer.
-     * @param first First value flag.
-     * @param addNames Names of additional values to be included.
-     * @param addVals Additional values to be included.
-     * @param addSens Sensitive flag of values or {@code null} if all values are not sensitive.
-     * @param addLen How many additional values will be included.
-     */
-    private static void appendVals(SBLimitedLength buf,
-        boolean first,
-        Object[] addNames,
-        Object[] addVals,
-        boolean[] addSens,
-        int addLen
-    ) {
-        if (addLen > 0) {
-            for (int i = 0; i < addLen; i++) {
-                Object addVal = addVals[i];
-
-                if (addVal != null) {
-                    if (addSens != null && addSens[i] && !includeSensitive())
-                        continue;
-
-                    GridToStringInclude incAnn = addVal.getClass().getAnnotation(GridToStringInclude.class);
-
-                    if (incAnn != null && incAnn.sensitive() && !includeSensitive())
-                        continue;
-                }
-
-                if (!first)
-                    buf.a(", ");
-                else
-                    first = false;
-
-                buf.a(addNames[i]).a('=');
-
-                toString(buf, addVal);
-            }
+        boolean isNew = GridToStringNode.init();
+        try {
+            List<GridToStringNode> addNodes =
+                    GridToStringNodeFactory.getNodes(propNames, propVals, propSens, propCnt);
+            GridToStringNode node = GridToStringNode.getRootNode(str, addNodes);
+            return isNew ? node.toString() : GridToStringNode.appendInnerBuffer(node);

Review Comment:
   Note the string-property path uses a *different* nested-call mechanism (`appendInnerBuffer` mutating the `LAST_CONSTRUCTED_..` node's `innerBuf`) than the object path (`markNode`). Two parallel, order-dependent mechanisms for 'nested call' is a lot of surface area and another place reentrancy can corrupt output.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1030,148 +773,33 @@ private static void handleOverflow(SBLimitedLength buf, int size) {
      */
     private static <T> String toStringImpl(
         Class<T> cls,
-        SBLimitedLength buf,
         T obj,
         Object[] addNames,
         Object[] addVals,
         @Nullable boolean[] addSens,
         int addLen) {
         assert cls != null;
-        assert buf != null;
         assert obj != null;
         assert addNames != null;
         assert addVals != null;
         assert addNames.length == addVals.length;
         assert addLen <= addNames.length;
-
-        boolean newStr = buf.length() == 0;
-
-        IdentityHashMap<Object, EntryReference> svdObjs = savedObjects.get();
-
-        if (newStr)
-            svdObjs.put(obj, new EntryReference(buf.length()));
-
+        boolean isNew = GridToStringNode.init();

Review Comment:
   The whole scheme depends on perfectly balanced `init()`/`clear()` across arbitrary nested user `toString()` calls; any imbalance leaks thread state into `CATCHED_NODES`. The original `savedObjects` map was balanced with a simple `put`/`remove(obj)` in `finally` per object and degraded gracefully — this global init/clear is much easier to get wrong.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);

Review Comment:
   The `offset < headLengthLimit` branch above got a `tail == null` guard (good — that's the actual NPE fix), but this `else` branch dereferences `tail` with no guard. If an insert ever targets an offset past the head while `tail` is still null, the same NPE class is back. Add the lazy-create here too, or assert the invariant.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");

Review Comment:
   `d(...)`, `r(...)`, `setLength(...)` now throw `UnsupportedOperationException`, widening the public surface of `GridStringBuilder` with throwing methods. `setLength` in particular was used by the old `toStringImpl` rollback path, so making it throw is a behavioral change. Another reason to keep this class minimal rather than expand it.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();

Review Comment:
   nit: `ArrayList` is a better default here (indexed iteration, less per-node overhead). `LinkedList` + `Collections.unmodifiableList` per object adds up on the hot path.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");

Review Comment:
   Any field-descriptor type not covered by the switch silently emits this placeholder into production logs. The original had no such trap. At minimum this should `assert`/throw in tests rather than ship a human-readable 'toString is not implemented yet' string to logs.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int idx) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder r(int start, int end, String str) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder nl() {
+        return a(CommonUtils.nl());
+    }
+
+    /** {@inheritDoc} */
+    @Override public int length() {
+        int length = super.length();
+        if (tail != null)
+            length += tail.getSkipped() + tail.length();
+        return length;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setLength(int len) {
+        throw new UnsupportedOperationException("setLength is not supported by this imlementation");

Review Comment:
   nit: typo `imlementation` -> `implementation`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int idx) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder r(int start, int end, String str) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder nl() {
+        return a(CommonUtils.nl());
+    }

Review Comment:
   Worth a careful look: `overflowed()` compares `length() >= HEAD_LEN`, but this overridden `length()` now includes `tail.getSkipped() + tail.length()`, while `onWrite`/`i(...)` compute substrings against head-only `impl().length()`. Mixing the composite `length()` with head-only `impl().length()` for position math is easy to get subtly wrong — please double-check the boundary arithmetic in `i(int, String)`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java:
##########
@@ -170,13 +157,102 @@ private CircularStringBuilder appendNull() {
         return this;
     }
 
+    /**
+     * Inserts a string into the buffer at the specified logical offset.
+     * This method is optimized to minimize the number of elements moved by choosing
+     * to shift elements from the closest end (left or right) to the insertion point.
+     *
+     * @param offset      The logical position (accounting for skipped characters)
+     *                    at which to insert.
+     * @param valToInsert The string to be inserted.
+     * @throws StringIndexOutOfBoundsException if the offset is invalid.
+     */
+    public void insert(int offset, String valToInsert) {
+        int curLength = length();
+        int offsetInsideBuf = offset - skipped;
+        if (offset < 0 || offsetInsideBuf > curLength)
+            throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength);
+        if (valToInsert == null)
+            valToInsert = "null";
+        int insertLength = valToInsert.length();
+        if (insertLength == 0)
+            return;
+        if (offsetInsideBuf == curLength) {
+            append(valToInsert);
+            return;
+        }
+        int spareSpace = value.length - curLength;
+        int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf);
+        if (insertCnt <= 0) {
+            skipped += valToInsert.length();
+            return;
+        }
+        int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0;
+        int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length;
+        int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset;
+        if (!full || offset - skipped > curLength / 2) {
+            shiftRight(insertCnt, moveRightCnt);
+            int charsToSkip = Math.max(0, insertCnt - spareSpace);
+            finishAt = (finishAt + insertCnt) % value.length;
+            shiftedOffset = (shiftedOffset + insertCnt) % value.length;
+            full = curLength + insertCnt >= value.length;
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += charsToSkip;
+        }
+        else {
+            int moveLeftCnt = (curLength - moveRightCnt - insertCnt);
+            shiftLeft(insertCnt, moveLeftCnt);
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += valToInsert.length();
+        }
+    }
+
     /**
      * @return Count of skipped elements.
      */
     public int getSkipped() {
         return skipped;
     }
 
+    /**
+     * Returns a substring from the logical sequence of characters, accounting for
+     * the circular buffer structure and any skipped characters.
+     *
+     * <p>This method first validates the indices against the total logical length
+     * (skipped + visible characters). If the requested range is empty or fully within
+     * the skipped portion, an empty string is returned for efficiency.
+     *
+     * <p>It then calculates the physical indices in the internal array. If the
+     * substring wraps around the end of the circular buffer, it performs a two-part
+     * copy operation to assemble the result.
+     *
+     * @param beginIdx the beginning index, inclusive.
+     * @param endIdx the ending index, exclusive.
+     * @return a new String containing the specified subsequence.
+     * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative,
+     *         or if endIdx is greater than the total logical length.
+     * @throws IllegalArgumentException if beginIdx is greater than endIdx.
+     */
+    public String substring(int beginIdx, int endIdx) {
+        if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length())
+            throw new StringIndexOutOfBoundsException(
+                    "Some of indexes is out of bounds. Begind index = " + beginIdx + " end index = " + endIdx);

Review Comment:
   nit: typo `Begind index` -> `Begin index`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.Optional;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+/**
+ * Abstract base class for nodes that tracks object references
+ * to prevent infinite recursion during string representation building.
+ */
+abstract class NodeRecursionMonitor extends GridToStringNode {
+    /** Thread-local registry to track objects currently being processed. */
+    static final ThreadLocal<IdentityHashMap<Object, NodeRecursionMonitor>> OBJECT_REGISTRY =
+            ThreadLocal.withInitial(IdentityHashMap::new);
+
+    /** The object being monitored for recursive references. */
+    private final Object obj;
+
+    /** Flag indicating if the identity hash code should be appended to the output. */
+    boolean hashIsRequired;
+
+    /**
+     * Constructor.
+     * @param propName Name of the property.
+     * @param obj Object to monitor for recursion.
+     */
+    NodeRecursionMonitor(String propName, Object obj) {
+        super(propName);
+        this.obj = obj;
+    }
+
+    /**
+     * Registers the current node in the thread-local registry for the given object.
+     * @param node The node to register.
+     */
+    void aqcuireRecursionMonitor(NodeRecursionMonitor node) {

Review Comment:
   nit: typo `aqcuireRecursionMonitor` -> `acquireRecursionMonitor`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {

Review Comment:
   This is the correct fix for the NPE. My suggestion is to ship *this* (the insert overrides + lazy `tail`) plus the `RecursivePayload` test as the entire patch, drop `recoverObject` from it, and split the node-tree work into a separate ticket/RFC.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481875637


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");

Review Comment:
   Ok, agree.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3482019192


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.Optional;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+/**
+ * Abstract base class for nodes that tracks object references
+ * to prevent infinite recursion during string representation building.
+ */
+abstract class NodeRecursionMonitor extends GridToStringNode {
+    /** Thread-local registry to track objects currently being processed. */
+    static final ThreadLocal<IdentityHashMap<Object, NodeRecursionMonitor>> OBJECT_REGISTRY =
+            ThreadLocal.withInitial(IdentityHashMap::new);
+
+    /** The object being monitored for recursive references. */
+    private final Object obj;
+
+    /** Flag indicating if the identity hash code should be appended to the output. */
+    boolean hashIsRequired;
+
+    /**
+     * Constructor.
+     * @param propName Name of the property.
+     * @param obj Object to monitor for recursion.
+     */
+    NodeRecursionMonitor(String propName, Object obj) {
+        super(propName);
+        this.obj = obj;
+    }
+
+    /**
+     * Registers the current node in the thread-local registry for the given object.
+     * @param node The node to register.
+     */
+    void aqcuireRecursionMonitor(NodeRecursionMonitor node) {

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3482083394


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {

Review Comment:
   I dropped recoverObject.
   I don't want to artificially modify the tests by removing valid cases just to satisfy the 'Fix NPE' requirement. I'd rather view this task as 'The handleRecursion method is not working correctly'."



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481272873


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1994,87 +1497,13 @@ public static <T> String joinToString(
         return buf.toString();
     }
 
-    /**
-     * Checks that object is already saved.
-     * In positive case this method inserts hash to the saved object entry (if needed) and name@hash for current entry.
-     * Further toString operations are not needed for current object.
-     *
-     * @param buf String builder buffer.
-     * @param obj Object.
-     * @param cls Class.
-     * @param svdObjs Map with saved objects to handle recursion.
-     * @return {@code True} if object is already saved and name@hash was added to buffer.
-     * {@code False} if it wasn't saved previously and it should be saved.
-     */
-    private static boolean handleRecursion(
-        SBLimitedLength buf,
-        Object obj,
-        @NotNull Class cls,
-        IdentityHashMap<Object, EntryReference> svdObjs
-    ) {
-        EntryReference ref = svdObjs.get(obj);
-
-        if (ref == null)
-            return false;
-
-        int pos = ref.pos;
-
-        String name = cls.getSimpleName();
-        String hash = identity(obj);
-        String savedName = name + hash;
-        String charsAtPos = buf.impl().substring(pos, pos + savedName.length());
-
-        if (!buf.isOverflowed() && !savedName.equals(charsAtPos)) {
-            if (charsAtPos.startsWith(cls.getSimpleName())) {
-                buf.i(pos + name.length(), hash);
-
-                incValues(svdObjs, obj, hash.length());
-            }
-            else
-                ref.hashNeeded = true;
-        }
-
-        buf.a(savedName);
-
-        return true;
-    }
-
-    /**
-     * Increment positions of already presented objects afterward given object.
-     *
-     * @param svdObjs Map with objects already presented in the buffer.
-     * @param obj Object.
-     * @param hashLen Length of the object's hash.
-     */
-    private static void incValues(IdentityHashMap<Object, EntryReference> svdObjs, Object obj, int hashLen) {
-        int baseline = svdObjs.get(obj).pos;
-
-        for (IdentityHashMap.Entry<Object, EntryReference> entry : svdObjs.entrySet()) {
-            EntryReference ref = entry.getValue();
-
-            int pos = ref.pos;
-
-            if (pos > baseline)
-                ref.pos = pos + hashLen;
-        }
-    }
-
-    /**
-     *
-     */
-    private static class EntryReference {
-        /** Position. */
-        int pos;
-
-        /** First object entry needs hash to be written. */
-        boolean hashNeeded;
-
-        /**
-         * @param pos Position.
-         */
-        private EntryReference(int pos) {
-            this.pos = pos;
-            hashNeeded = false;
-        }
+    /** */
+    private static <T extends Throwable> String handleThrowable(T throwable) {
+        StringWriter strWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(strWriter);
+        throwable.printStackTrace(printWriter);
+        printWriter.flush();
+        printWriter.close();
+        return strWriter.toString();
     }

Review Comment:
   Unfortunately it's the only way to show exception if it happens



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "anton-vinogradov (via GitHub)" <gi...@apache.org>.
anton-vinogradov commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3483046206


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();

Review Comment:
   "Unknown capacity" argues *for* `ArrayList`, not against it: `ArrayList` handles unknown size fine via amortized doubling and is cheaper per element (a single backing array vs. a node object + two links each), and this list is built once and then only iterated. `LinkedList` only pays off for frequent head/middle insert-remove, which doesn't happen here.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT;
+import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode;
+
+/**
+ * A node that represents an array in the string representation.
+ * It creates nodes for each element of the array and formats the output with square brackets.
+ */
+class GridToStringArrayNode extends NodeRecursionMonitor {
+    /** An array of child nodes, each representing an element of the source array. */
+    private final GridToStringNode[] nodes;
+
+    /** The rule for appending a hint about skipped elements if the array is too large. */
+    private final LongSequenceSkipRule skipRule;
+
+    /** The class object representing the type of the array. */
+    private final Class<?> arrType;
+
+    /**
+     * Constructs a new array node.
+     * Iterates over the input array, creates a node for each element,
+     * and populates the internal array up to the collection size limit.
+     * @param propName The property name.
+     * @param arr The source array.
+     * @param arrType The class object of the array's type.
+     */
+    GridToStringArrayNode(String propName, Object[] arr, Class<?> arrType) {
+        super(propName, arr);
+        try {
+            aqcuireRecursionMonitor(this);
+            this.arrType = arrType;
+            skipRule = new LongSequenceSkipRule(() -> arr.length);
+            nodes = new GridToStringNode[Math.min(COLLECTION_LIMIT, arr.length)];
+            for (int i = 0; i < nodes.length; i++) {
+                final int idx = i;
+                nodes[idx] = getGridToStringNode(null, () -> arr[idx], () -> arr[idx].getClass());

Review Comment:
   You're right — `getGridToStringNode` calls `valSupplier.get()` and returns a `GridToStringNullNode` before the class supplier is ever invoked, and this is a lambda (`() -> child.getClass()`), not a bound method reference, so it can't NPE on a null element. Withdrawing this one. 👍



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481308946


##########
modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java:
##########
@@ -204,6 +205,106 @@ public void testToStringCheckObjectRecursionPrevention() throws Exception {
         fut4.get(3_000);
     }
 
+
+    /** */
+    @Test
+    public void testNPE() {
+        boolean success;
+        try {
+            SBLimitedLength sbLimitedLength = new SBLimitedLength(256);
+            sbLimitedLength.initLimit(new SBLengthLimit());
+            sbLimitedLength.a("a".repeat(7999));
+            sbLimitedLength.i(7000, "asd");
+            sbLimitedLength.a("a".repeat(10));
+            success = true;
+        } catch (NullPointerException ignored) {
+            success = false;
+        }
+        Assert.assertTrue(success);
+    }

Review Comment:
   We have Junit4, it has no assertThrows assertion, it's bypass



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481836597


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1030,148 +773,33 @@ private static void handleOverflow(SBLimitedLength buf, int size) {
      */
     private static <T> String toStringImpl(
         Class<T> cls,
-        SBLimitedLength buf,
         T obj,
         Object[] addNames,
         Object[] addVals,
         @Nullable boolean[] addSens,
         int addLen) {
         assert cls != null;
-        assert buf != null;
         assert obj != null;
         assert addNames != null;
         assert addVals != null;
         assert addNames.length == addVals.length;
         assert addLen <= addNames.length;
-
-        boolean newStr = buf.length() == 0;
-
-        IdentityHashMap<Object, EntryReference> svdObjs = savedObjects.get();
-
-        if (newStr)
-            svdObjs.put(obj, new EntryReference(buf.length()));
-
+        boolean isNew = GridToStringNode.init();

Review Comment:
   yes, it's not difficult, any init call in finally clause should call clear(). I thought about try with resources clause, but it looks not good



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3482032956


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java:
##########
@@ -170,13 +157,102 @@ private CircularStringBuilder appendNull() {
         return this;
     }
 
+    /**
+     * Inserts a string into the buffer at the specified logical offset.
+     * This method is optimized to minimize the number of elements moved by choosing
+     * to shift elements from the closest end (left or right) to the insertion point.
+     *
+     * @param offset      The logical position (accounting for skipped characters)
+     *                    at which to insert.
+     * @param valToInsert The string to be inserted.
+     * @throws StringIndexOutOfBoundsException if the offset is invalid.
+     */
+    public void insert(int offset, String valToInsert) {
+        int curLength = length();
+        int offsetInsideBuf = offset - skipped;
+        if (offset < 0 || offsetInsideBuf > curLength)
+            throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength);
+        if (valToInsert == null)
+            valToInsert = "null";
+        int insertLength = valToInsert.length();
+        if (insertLength == 0)
+            return;
+        if (offsetInsideBuf == curLength) {
+            append(valToInsert);
+            return;
+        }
+        int spareSpace = value.length - curLength;
+        int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf);
+        if (insertCnt <= 0) {
+            skipped += valToInsert.length();
+            return;
+        }
+        int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0;
+        int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length;
+        int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset;
+        if (!full || offset - skipped > curLength / 2) {
+            shiftRight(insertCnt, moveRightCnt);
+            int charsToSkip = Math.max(0, insertCnt - spareSpace);
+            finishAt = (finishAt + insertCnt) % value.length;
+            shiftedOffset = (shiftedOffset + insertCnt) % value.length;
+            full = curLength + insertCnt >= value.length;
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += charsToSkip;
+        }
+        else {
+            int moveLeftCnt = (curLength - moveRightCnt - insertCnt);
+            shiftLeft(insertCnt, moveLeftCnt);
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += valToInsert.length();
+        }
+    }
+
     /**
      * @return Count of skipped elements.
      */
     public int getSkipped() {
         return skipped;
     }
 
+    /**
+     * Returns a substring from the logical sequence of characters, accounting for
+     * the circular buffer structure and any skipped characters.
+     *
+     * <p>This method first validates the indices against the total logical length
+     * (skipped + visible characters). If the requested range is empty or fully within
+     * the skipped portion, an empty string is returned for efficiency.
+     *
+     * <p>It then calculates the physical indices in the internal array. If the
+     * substring wraps around the end of the circular buffer, it performs a two-part
+     * copy operation to assemble the result.
+     *
+     * @param beginIdx the beginning index, inclusive.
+     * @param endIdx the ending index, exclusive.
+     * @return a new String containing the specified subsequence.
+     * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative,
+     *         or if endIdx is greater than the total logical length.
+     * @throws IllegalArgumentException if beginIdx is greater than endIdx.
+     */
+    public String substring(int beginIdx, int endIdx) {
+        if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length())
+            throw new StringIndexOutOfBoundsException(
+                    "Some of indexes is out of bounds. Begind index = " + beginIdx + " end index = " + endIdx);

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3482015747


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();

Review Comment:
   I don't know what capacity this List would have, so I used LinkedList



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3481732050


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1030,148 +773,33 @@ private static void handleOverflow(SBLimitedLength buf, int size) {
      */
     private static <T> String toStringImpl(
         Class<T> cls,
-        SBLimitedLength buf,
         T obj,
         Object[] addNames,
         Object[] addVals,
         @Nullable boolean[] addSens,
         int addLen) {
         assert cls != null;
-        assert buf != null;
         assert obj != null;
         assert addNames != null;
         assert addVals != null;
         assert addNames.length == addVals.length;
         assert addLen <= addNames.length;
-
-        boolean newStr = buf.length() == 0;
-
-        IdentityHashMap<Object, EntryReference> svdObjs = savedObjects.get();
-
-        if (newStr)
-            svdObjs.put(obj, new EntryReference(buf.length()));
-
+        boolean isNew = GridToStringNode.init();
         try {
-            int len = buf.length();
-
-            String s = toStringImpl0(cls, buf, obj, addNames, addVals, addSens, addLen);
-
-            if (newStr)
-                return s;
-
-            buf.setLength(len);
-
-            return s.substring(len);
+            List<GridToStringNode> addNodes =
+                    GridToStringNodeFactory.getNodes(addNames, addVals, addSens, addLen);
+            GridToStringNode node = GridToStringNode.getRootNode(obj, cls, addNodes);
+            return isNew ? node.toString() : GridToStringNode.markNode(node);

Review Comment:
   Ok, Agree, now it returns toString result but still holds marker for whole decision.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "EgorBaranovEnjoysTyping (via GitHub)" <gi...@apache.org>.
EgorBaranovEnjoysTyping commented on PR #13262:
URL: https://github.com/apache/ignite/pull/13262#issuecomment-4830824180

       Fix Summary
   
       1. NPE in handleRecursion
       Root cause: handleRecursion(SBLimitedLength buf, Object obj, Class cls, IdentityHashMap<Object, EntryReference> svdObjs) accesses buf.impl().substring(pos, pos + savedName.length()) where pos comes from
       EntryReference.pos. When an object is encountered for the first time, svdObjs.get(obj) returns null, but the method returns early. However, when the buffer is shared across nested toString() calls (via
       ThreadLocal<SBLimitedLength>), the pos stored in EntryReference can point to an invalid offset after the buffer is reset or reused, causing NullPointerException when buf.impl() returns null.
   
       Fix: Replaced the mutable buffer-based approach with a tree-of-nodes architecture (GridToStringNode). Recursion is tracked via NodeRecursionMonitor (ThreadLocal registry), and each node appends itself
       independently — no shared mutable buffer, no position tracking.
   
       ---
   
       2. Inconsistent identity hash assignment
       Root cause: In the original code, the identity hash of an object was only inserted into the buffer at a previously recorded position (buf.i(pos + name.length(), hash)). When toString() was called directly
       on an object (top-level), ref.hashNeeded was set to true, but the hash was never appended because there was no prior occurrence at a known position. However, when the same object appeared nested inside
       another object's toString(), the hash was inserted for both the recursive termination node and the original occurrence. This resulted in inconsistent output: the top-level object had no hash, while nested
       objects did.
   
       Fix: The new GridToStringRecursionTerminationNode immediately marks the parent NodeRecursionMonitor with setHashRequired(), and the parent node appends its hash during appendNode() via
       appendHashIfRequired(sb). This ensures consistent hash output regardless of call depth.
   
       ---
   
       3. StringIndexOutOfBoundsException in handleRecursion
       Root cause: handleRecursion calculates charsAtPos = buf.impl().substring(pos, pos + savedName.length()) where savedName = cls.getSimpleName() + identity(obj). If pos + savedName.length() exceeds the
       current buffer length, substring() throws StringIndexOutOfBoundsException. This happens when the buffer has grown past pos but not enough to contain the full savedName string at that offset, or when the
       buffer was truncated due to the head limit.
   
       Fix: Eliminated position-based buffer manipulation entirely. The node tree approach builds the string representation by traversing nodes, so no substring operations at arbitrary positions are needed.
   
       ---
   
       4. Incorrect string truncation at the head-tail boundary in SBLengthLimit
       Root cause: The original onWrite method used Math.min(sb.length(), HEAD_LEN + 1) to determine the head length when creating the tail. The overflowed() check used sb.impl().length() > HEAD_LEN (strict
       greater-than), which caused an off-by-one error: the head could exceed HEAD_LEN by one character before the tail was created. Additionally, sb.setLength(newSbLen) was called, but the parent class's
       setLength behavior didn't account for the tail split correctly, resulting in characters being lost or duplicated at the boundary.
   
       Fix: Changed overflowed() to use >= (sb.length() >= getHeadLengthLimit()), replaced HEAD_LEN + 1 with getHeadLengthLimit(), and used sb.impl().setLength() directly to avoid interference from overridden
       methods. Added length() override in SBLimitedLength that correctly accounts for tail.getSkipped() + tail.length().
   
       ---
   
       5. Missing insert() methods in SBLimitedLength and CircularStringBuilder
       Root cause: SBLimitedLength didn't override the insert() family of methods from GridStringBuilder. When handleRecursion called buf.i(pos + name.length(), hash) to insert the identity hash, the parent
       class's insert() operated on the underlying buffer without respecting the head-tail split, corrupting the output or causing StringIndexOutOfBoundsException.
   
       Fix: Implemented i(int offset, String str) in SBLimitedLength that correctly redirects inserts to the tail when the offset is beyond the head limit. Implemented insert(int offset, String valToInsert) in
       CircularStringBuilder with proper circular buffer handling. Methods that delete/modify the head (d(), r(), setLength()) explicitly throw UnsupportedOperationException.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Colle… [ignite]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #13262:
URL: https://github.com/apache/ignite/pull/13262#discussion_r3467698668


##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java:
##########
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringNode.identities;
+
+/**
+ * A factory class responsible for creating appropriate GridToStringNode instances
+ * based on the type and value of the object being processed.
+ */
+public class GridToStringNodeFactory {
+    /**
+     * Creates a list of nodes from arrays of property names and values.
+     * Handles sensitive data exclusion and node reuse from a thread-local cache.
+     * @param addNames Array of property names.
+     * @param addVals Array of property values.
+     * @param addSens Array of flags indicating if a property is sensitive.
+     * @param addLen The number of elements to process.
+     * @return A list of constructed GridToStringNode objects.
+     */
+    public static List<GridToStringNode> getNodes(Object[] addNames,
+                                                  Object[] addVals,
+                                                  boolean[] addSens,
+                                                  int addLen) {
+        List<GridToStringNode> result = new LinkedList<>();
+        boolean includeSensitive = GridToStringBuilder.includeSensitive();
+        for (int i = 0; i < addLen; i++) {
+            if (!includeSensitive && shouldBeExcluded(addVals, addSens, i))
+                continue;
+            String propName = String.valueOf(addNames[i]);
+            GridToStringNode node = recoverOrCreate(propName, addVals[i]);
+            result.add(node);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a node for a field based on its descriptor and the parent object.
+     * This method acts as a dispatcher, routing the creation logic based on the field's type.
+     * @param obj The parent object containing the field.
+     * @param fd The descriptor of the field to be processed.
+     * @return A new GridToStringNode for the field's value.
+     */
+    static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) {
+        String childPropName = fd.getName();
+        if (obj == null)
+            return new GridToStringNullNode(childPropName);
+        return switch (fd.type()) {
+            case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> {
+                Supplier<Class<?>> fieldClsSupplier = () -> Optional.of(fd)
+                        .map(GridToStringFieldDescriptor::fieldClass)
+                        .map(Class.class::cast)
+                        .orElseGet(obj::getClass);
+                yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier);
+            }
+            case GridToStringFieldDescriptor.FIELD_TYPE_BYTE ->
+                    getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN ->
+                    getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_CHAR ->
+                    getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_SHORT ->
+                    getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_INT ->
+                    getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT ->
+                    getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_LONG ->
+                    getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class);
+            case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE ->
+                    getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class);
+            default -> new GridToStringValueNode(childPropName, "toString is not implemented yet");
+        };
+    }
+
+    /**
+     * The core factory method that creates a node for a given value and its class.
+     * It is the central point for determining the correct node type for any object.
+     * Handles nulls, recursion, primitives, arrays, collections, maps, and standard objects.
+     * @param childPropName The property name for the new node.
+     * @param valSupplier A supplier to lazily retrieve the value.
+     * @param childFieldClsSupplier A supplier to lazily retrieve the class of the value.
+     * @return A new GridToStringNode appropriate for the value.
+     */
+    static GridToStringNode getGridToStringNode(String childPropName,
+                                                Supplier<Object> valSupplier,
+                                                Supplier<Class<?>> childFieldClsSupplier) {
+        Object val = valSupplier.get();
+        if (val == null)
+            return new GridToStringNullNode(childPropName);
+        Optional<GridToStringNode> recursionTermination = NodeRecursionMonitor.findRecursionMonitor(val)
+                .map(monitor -> GridToStringRecursionTerminationNode.of(monitor, val));
+        if (recursionTermination.isPresent())
+            return recursionTermination.get();
+        Class<?> childFieldCls = childFieldClsSupplier.get();
+        if (childFieldCls.isPrimitive())
+            return new GridToStringValueNode(childPropName, val);
+        else if (childFieldCls.isArray())
+            return new GridToStringArrayNode(childPropName, (Object[])val, childFieldCls);

Review Comment:
   This will throw `ClassCastException` for primitive arrays (e.g., `int[]` cannot be cast to `Object[]`). The array branch needs to handle primitive arrays separately (e.g., by converting via the existing primitive-array formatting logic) and only cast to `Object[]` for reference-type arrays.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }

Review Comment:
   These `i(...)` overloads bypass `SBLimitedLength`'s custom insert routing by calling `super.i(...)`, which inserts directly into the head buffer and ignores head/tail limiting. They should delegate to this class’s `i(int, String)` (or equivalent) so inserts beyond the head limit are correctly redirected/handled.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringCollectionNode.java:
##########
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT;
+import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode;
+
+/**
+ * A node that represents a collection (e.g., List, Set) in the string representation.
+ * It creates nodes for each element and formats the output with square brackets.
+ */
+class GridToStringCollectionNode extends NodeRecursionMonitor {
+    /** A list of child nodes, each representing an element of the collection. */
+    private final Collection<GridToStringNode> col;
+
+    /** The simple class name of the collection being represented. */
+    private final String colSimpleClsName;
+
+    /** The rule for appending a hint about skipped elements if the collection is too large. */
+    private final LongSequenceSkipRule skipRule;
+
+    /**
+     * Constructs a new collection node.
+     * Iterates over the input collection, creates a node for each element,
+     * and populates the internal list up to the collection size limit.
+     * @param propName The property name.
+     * @param col The collection to represent.
+     */
+    GridToStringCollectionNode(String propName, Collection<?> col) {
+        super(propName, col);
+        try {
+            aqcuireRecursionMonitor(this);
+            colSimpleClsName = col.getClass().getSimpleName();
+            this.col = new ArrayList<>(Math.min(col.size(), COLLECTION_LIMIT));
+            skipRule = new LongSequenceSkipRule(col::size);
+            Iterator<?> iter = col.iterator();
+            while (iter.hasNext() && this.col.size() != COLLECTION_LIMIT) {
+                Object obj = iter.next();
+                GridToStringNode node = getGridToStringNode(null, () -> obj, obj::getClass);

Review Comment:
   If the collection contains `null`, `obj::getClass` will throw at method-reference creation time. Use a null-safe class supplier (e.g., `() -> obj != null ? obj.getClass() : Object.class`) and let `getGridToStringNode` produce a `GridToStringNullNode` for null values.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.Optional;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+/**
+ * Abstract base class for nodes that tracks object references
+ * to prevent infinite recursion during string representation building.
+ */
+abstract class NodeRecursionMonitor extends GridToStringNode {
+    /** Thread-local registry to track objects currently being processed. */
+    static final ThreadLocal<IdentityHashMap<Object, NodeRecursionMonitor>> OBJECT_REGISTRY =
+            ThreadLocal.withInitial(IdentityHashMap::new);
+
+    /** The object being monitored for recursive references. */
+    private final Object obj;
+
+    /** Flag indicating if the identity hash code should be appended to the output. */
+    boolean hashIsRequired;
+
+    /**
+     * Constructor.
+     * @param propName Name of the property.
+     * @param obj Object to monitor for recursion.
+     */
+    NodeRecursionMonitor(String propName, Object obj) {
+        super(propName);
+        this.obj = obj;
+    }
+
+    /**
+     * Registers the current node in the thread-local registry for the given object.
+     * @param node The node to register.
+     */
+    void aqcuireRecursionMonitor(NodeRecursionMonitor node) {
+        OBJECT_REGISTRY.get().put(obj, node);
+    }

Review Comment:
   Typo in method name: `aqcuireRecursionMonitor` → `acquireRecursionMonitor`. Also, the `node` parameter is redundant (the method can register `this`) which would simplify call sites.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java:
##########
@@ -270,6 +264,124 @@ private GridStringBuilder onWrite(int lenBeforeWrite) {
         return onWrite(curLen);
     }
 
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int offset, String str) {
+        str = GridToStringNode.recoverObject(str);
+        int headLengthLimit = lenLimit.getHeadLengthLimit();
+        if (offset < headLengthLimit) {
+            impl().insert(offset, str);
+            if (lenLimit.overflowed(this)) {
+                String tailCandidate = impl().substring(headLengthLimit);
+                if (tail == null)
+                    tail = lenLimit.createTail();
+                tail.insert(0, tailCandidate);
+                impl().setLength(headLengthLimit);
+            }
+            return this;
+        }
+        tail.insert(offset - headLengthLimit, str);
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int idx, char[] str, int off, int len) {
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < len; i++)
+            strBuilder.append(str[i + off]);
+        return i(idx, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char[] str) {
+        return i(off, new String(str));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = 0; i < s.length(); i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        s = GridToStringNode.recoverObject(s);
+        StringBuilder strBuilder = new StringBuilder();
+        for (int i = start; i < end; i++)
+            strBuilder.append(s.charAt(i));
+        return i(dstOff, strBuilder.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, boolean b) {
+        return super.i(off, String.valueOf(b));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, char c) {
+        return super.i(off, String.valueOf(c));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, int i) {
+        return super.i(off, String.valueOf(i));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, long l) {
+        return super.i(off, String.valueOf(l));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, float f) {
+        return super.i(off, String.valueOf(f));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder i(int off, double d) {
+        return super.i(off, String.valueOf(d));
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int start, int end) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder d(int idx) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder r(int start, int end, String str) {
+        throw new UnsupportedOperationException("Not supported by this implementation");
+    }
+    
+    /** {@inheritDoc} */
+    @Override public GridStringBuilder nl() {
+        return a(CommonUtils.nl());
+    }
+
+    /** {@inheritDoc} */
+    @Override public int length() {
+        int length = super.length();
+        if (tail != null)
+            length += tail.getSkipped() + tail.length();
+        return length;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setLength(int len) {
+        throw new UnsupportedOperationException("setLength is not supported by this imlementation");
+    }

Review Comment:
   Typo in exception message: `imlementation` → `implementation`.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java:
##########
@@ -1994,87 +1497,13 @@ public static <T> String joinToString(
         return buf.toString();
     }
 
-    /**
-     * Checks that object is already saved.
-     * In positive case this method inserts hash to the saved object entry (if needed) and name@hash for current entry.
-     * Further toString operations are not needed for current object.
-     *
-     * @param buf String builder buffer.
-     * @param obj Object.
-     * @param cls Class.
-     * @param svdObjs Map with saved objects to handle recursion.
-     * @return {@code True} if object is already saved and name@hash was added to buffer.
-     * {@code False} if it wasn't saved previously and it should be saved.
-     */
-    private static boolean handleRecursion(
-        SBLimitedLength buf,
-        Object obj,
-        @NotNull Class cls,
-        IdentityHashMap<Object, EntryReference> svdObjs
-    ) {
-        EntryReference ref = svdObjs.get(obj);
-
-        if (ref == null)
-            return false;
-
-        int pos = ref.pos;
-
-        String name = cls.getSimpleName();
-        String hash = identity(obj);
-        String savedName = name + hash;
-        String charsAtPos = buf.impl().substring(pos, pos + savedName.length());
-
-        if (!buf.isOverflowed() && !savedName.equals(charsAtPos)) {
-            if (charsAtPos.startsWith(cls.getSimpleName())) {
-                buf.i(pos + name.length(), hash);
-
-                incValues(svdObjs, obj, hash.length());
-            }
-            else
-                ref.hashNeeded = true;
-        }
-
-        buf.a(savedName);
-
-        return true;
-    }
-
-    /**
-     * Increment positions of already presented objects afterward given object.
-     *
-     * @param svdObjs Map with objects already presented in the buffer.
-     * @param obj Object.
-     * @param hashLen Length of the object's hash.
-     */
-    private static void incValues(IdentityHashMap<Object, EntryReference> svdObjs, Object obj, int hashLen) {
-        int baseline = svdObjs.get(obj).pos;
-
-        for (IdentityHashMap.Entry<Object, EntryReference> entry : svdObjs.entrySet()) {
-            EntryReference ref = entry.getValue();
-
-            int pos = ref.pos;
-
-            if (pos > baseline)
-                ref.pos = pos + hashLen;
-        }
-    }
-
-    /**
-     *
-     */
-    private static class EntryReference {
-        /** Position. */
-        int pos;
-
-        /** First object entry needs hash to be written. */
-        boolean hashNeeded;
-
-        /**
-         * @param pos Position.
-         */
-        private EntryReference(int pos) {
-            this.pos = pos;
-            hashNeeded = false;
-        }
+    /** */
+    private static <T extends Throwable> String handleThrowable(T throwable) {
+        StringWriter strWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(strWriter);
+        throwable.printStackTrace(printWriter);
+        printWriter.flush();
+        printWriter.close();
+        return strWriter.toString();
     }

Review Comment:
   Returning full stack traces from `toString()` can leak internal details and potentially sensitive data into logs and monitoring outputs. Consider returning a sanitized/summary string (e.g., exception class + message) and optionally logging the full stack trace through the project’s logging facilities.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringMapNode.java:
##########
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT;
+import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode;
+
+/**
+ * A node that represents a map (key-value pairs) in the string representation.
+ * It creates nodes for each key and value and formats the output with curly braces.
+ */
+class GridToStringMapNode extends NodeRecursionMonitor {
+    /** An internal map that stores key-value pairs as GridToStringNode instances. */
+    private final Map<GridToStringNode, GridToStringNode> map;
+
+    /** The simple class name of the map being represented. */
+    private final String mapClsSimpleName;
+
+    /** The rule for appending a hint about skipped elements if the map is too large. */
+    private final LongSequenceSkipRule skipRule;
+
+    /**
+     * Constructs a new map node.
+     * Iterates over the entries of the input map, creates nodes for keys and values,
+     * and populates the internal map structure up to the collection size limit.
+     * @param propName The property name.
+     * @param map The map to represent.
+     */
+    GridToStringMapNode(String propName, Map<?, ?> map) {
+        super(propName, map);
+        try {
+            aqcuireRecursionMonitor(this);
+            mapClsSimpleName = map.getClass().getSimpleName();
+            this.map = new HashMap<>();
+            skipRule = new LongSequenceSkipRule(map::size);
+            Iterator<? extends Map.Entry<?, ?>> iter = map.entrySet().iterator();
+            while (iter.hasNext() && this.map.size() != COLLECTION_LIMIT) {
+                Map.Entry<?, ?> entry = iter.next();
+                Object key = entry.getKey();
+                Object val = entry.getValue();
+                GridToStringNode keyNode = getGridToStringNode(null, () -> key, key::getClass);
+                GridToStringNode valNode = getGridToStringNode(null, () -> val, val::getClass);

Review Comment:
   Maps can contain null keys/values; `key::getClass` / `val::getClass` will throw `NullPointerException` in that case. Use null-safe class suppliers (or route nulls to `GridToStringNullNode`) so map stringification is robust.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java:
##########
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT;
+import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode;
+
+/**
+ * A node that represents an array in the string representation.
+ * It creates nodes for each element of the array and formats the output with square brackets.
+ */
+class GridToStringArrayNode extends NodeRecursionMonitor {
+    /** An array of child nodes, each representing an element of the source array. */
+    private final GridToStringNode[] nodes;
+
+    /** The rule for appending a hint about skipped elements if the array is too large. */
+    private final LongSequenceSkipRule skipRule;
+
+    /** The class object representing the type of the array. */
+    private final Class<?> arrType;
+
+    /**
+     * Constructs a new array node.
+     * Iterates over the input array, creates a node for each element,
+     * and populates the internal array up to the collection size limit.
+     * @param propName The property name.
+     * @param arr The source array.
+     * @param arrType The class object of the array's type.
+     */
+    GridToStringArrayNode(String propName, Object[] arr, Class<?> arrType) {
+        super(propName, arr);
+        try {
+            aqcuireRecursionMonitor(this);
+            this.arrType = arrType;
+            skipRule = new LongSequenceSkipRule(() -> arr.length);
+            nodes = new GridToStringNode[Math.min(COLLECTION_LIMIT, arr.length)];
+            for (int i = 0; i < nodes.length; i++) {
+                final int idx = i;
+                nodes[idx] = getGridToStringNode(null, () -> arr[idx], () -> arr[idx].getClass());

Review Comment:
   If an array element is `null`, `arr[idx].getClass()` will throw `NullPointerException` during node construction. Use a null-safe class supplier (e.g., return `Object.class` when the value is null), relying on `getGridToStringNode`’s existing null handling for the value itself.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java:
##########
@@ -35,50 +35,44 @@ class SBLengthLimit {
     /** Length of head part of message. */
     private static final int HEAD_LEN = MAX_TO_STR_LEN - TAIL_LEN;
 
-    /** */
-    private int len;
-
-    /**
-     * @return Current length.
-     */
-    int length() {
-        return len;
-    }
-
-    /**
-     *
-     */
-    void reset() {
-        len = 0;
-    }
-
     /**
      * @param sb String builder.
      * @param writtenLen Written length.
      */
     void onWrite(SBLimitedLength sb, int writtenLen) {
-        len += writtenLen;
-
         if (overflowed(sb) && (sb.getTail() == null || sb.getTail().length() == 0)) {
-            CircularStringBuilder tail = getTail();
-
-            int newSbLen = Math.min(sb.length(), HEAD_LEN + 1);
+            CircularStringBuilder tail = createTail();
+            int newSbLen = Math.min(sb.length(), getHeadLengthLimit());
             tail.append(sb.impl().substring(newSbLen));
-
             sb.setTail(tail);
-            sb.setLength(newSbLen);
+            sb.impl().setLength(newSbLen);
         }
     }
 
-    /** */
-    CircularStringBuilder getTail() {
-        return new CircularStringBuilder(TAIL_LEN);
+    /** Creates empty tail
+     * @return empty tail */
+    CircularStringBuilder createTail() {
+        return new CircularStringBuilder(getTailLengthLimit());
     }
 
     /**
-     * @return {@code True} if reached limit.
+     * @return {@code True} if this string builder exceeds limit, false otherwise
      */
     boolean overflowed(SBLimitedLength sb) {
-        return sb.impl().length() > HEAD_LEN;
+        return sb.length() >= getHeadLengthLimit();
+    }

Review Comment:
   `overflowed` is using `sb.length()` (which includes tail + skipped) and `>=`, which can mark the builder overflowed too early (exactly at the head limit) and can also keep it permanently overflowed once a tail exists. This should be based on the head buffer length (`sb.impl().length()`) and should match the intended threshold (typically `>` the head limit).



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java:
##########
@@ -170,13 +157,102 @@ private CircularStringBuilder appendNull() {
         return this;
     }
 
+    /**
+     * Inserts a string into the buffer at the specified logical offset.
+     * This method is optimized to minimize the number of elements moved by choosing
+     * to shift elements from the closest end (left or right) to the insertion point.
+     *
+     * @param offset      The logical position (accounting for skipped characters)
+     *                    at which to insert.
+     * @param valToInsert The string to be inserted.
+     * @throws StringIndexOutOfBoundsException if the offset is invalid.
+     */
+    public void insert(int offset, String valToInsert) {
+        int curLength = length();
+        int offsetInsideBuf = offset - skipped;
+        if (offset < 0 || offsetInsideBuf > curLength)
+            throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength);
+        if (valToInsert == null)
+            valToInsert = "null";
+        int insertLength = valToInsert.length();
+        if (insertLength == 0)
+            return;
+        if (offsetInsideBuf == curLength) {
+            append(valToInsert);
+            return;
+        }
+        int spareSpace = value.length - curLength;
+        int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf);
+        if (insertCnt <= 0) {
+            skipped += valToInsert.length();
+            return;
+        }
+        int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0;
+        int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length;
+        int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset;
+        if (!full || offset - skipped > curLength / 2) {
+            shiftRight(insertCnt, moveRightCnt);
+            int charsToSkip = Math.max(0, insertCnt - spareSpace);
+            finishAt = (finishAt + insertCnt) % value.length;
+            shiftedOffset = (shiftedOffset + insertCnt) % value.length;
+            full = curLength + insertCnt >= value.length;
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += charsToSkip;
+        }
+        else {
+            int moveLeftCnt = (curLength - moveRightCnt - insertCnt);
+            shiftLeft(insertCnt, moveLeftCnt);
+            insertStringTail(valToInsert, shiftedOffset, insertCnt);
+            skipped += valToInsert.length();
+        }
+    }
+
     /**
      * @return Count of skipped elements.
      */
     public int getSkipped() {
         return skipped;
     }
 
+    /**
+     * Returns a substring from the logical sequence of characters, accounting for
+     * the circular buffer structure and any skipped characters.
+     *
+     * <p>This method first validates the indices against the total logical length
+     * (skipped + visible characters). If the requested range is empty or fully within
+     * the skipped portion, an empty string is returned for efficiency.
+     *
+     * <p>It then calculates the physical indices in the internal array. If the
+     * substring wraps around the end of the circular buffer, it performs a two-part
+     * copy operation to assemble the result.
+     *
+     * @param beginIdx the beginning index, inclusive.
+     * @param endIdx the ending index, exclusive.
+     * @return a new String containing the specified subsequence.
+     * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative,
+     *         or if endIdx is greater than the total logical length.
+     * @throws IllegalArgumentException if beginIdx is greater than endIdx.
+     */
+    public String substring(int beginIdx, int endIdx) {
+        if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length())
+            throw new StringIndexOutOfBoundsException(
+                    "Some of indexes is out of bounds. Begind index = " + beginIdx + " end index = " + endIdx);
+        if (beginIdx > endIdx)
+            throw new IllegalArgumentException(
+                    "Begin index can not be greater then end index (begin = " + beginIdx + " end = " + endIdx + ")");

Review Comment:
   Exception messages contain multiple typos/grammar issues (e.g., \"Some of indexes\", \"Begind\", \"can not\", \"then\" vs \"than\"). Please correct them to improve diagnosability.



##########
modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java:
##########
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.tostring;
+
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.ignite.internal.util.GridStringBuilder;
+
+import static org.apache.ignite.internal.util.tostring.NodeRecursionMonitor.OBJECT_REGISTRY;
+
+/**
+ * The abstract base class for all nodes in the string representation tree.
+ * Defines the common interface for appending a node's value to a string builder
+ * and provides static factory and utility methods for all subclasses.
+ */
+public abstract class GridToStringNode {
+    /**
+     * A thread-local cache for nodes, used to handle references of
+     * inner toString() calls by mapping temporary markers to actual nodes.
+     */
+    public static final ConcurrentHashMap<Thread, IdentityHashMap<String, GridToStringNode>> CATCHED_NODES
+            = new ConcurrentHashMap<>();

Review Comment:
   `CATCHED_NODES` appears to be a misspelling and reduces readability/maintainability. Consider renaming to `CACHED_NODES` (and updating references). If binary compatibility matters, consider keeping the old name as a deprecated alias.



##########
modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java:
##########
@@ -204,6 +205,106 @@ public void testToStringCheckObjectRecursionPrevention() throws Exception {
         fut4.get(3_000);
     }
 
+
+    /** */
+    @Test
+    public void testNPE() {
+        boolean success;
+        try {
+            SBLimitedLength sbLimitedLength = new SBLimitedLength(256);
+            sbLimitedLength.initLimit(new SBLengthLimit());
+            sbLimitedLength.a("a".repeat(7999));
+            sbLimitedLength.i(7000, "asd");
+            sbLimitedLength.a("a".repeat(10));
+            success = true;
+        } catch (NullPointerException ignored) {
+            success = false;
+        }
+        Assert.assertTrue(success);
+    }

Review Comment:
   This test should fail naturally if an exception is thrown; wrapping in a boolean + catching only `NullPointerException` can hide other failures and reduces usefulness of the stack trace. Prefer an assertion that the block does not throw (or omit the try/catch entirely) so any exception fails the test with full diagnostics.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org