You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ignite.apache.org by "FewJuho (via GitHub)" <gi...@apache.org> on 2025/07/21 14:18:07 UTC

[PR] IGNITE-21899 added GridInt utils [ignite-teamcity-bot]

FewJuho opened a new pull request, #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200

   (no comment)


-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315481655


##########
migrator/src/main/java/migrate/Transformer.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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 src.main.java.migrate;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+ * Guarded by a MAX_DEPTH to avoid accidental cycles.
+ */
+public final class Transformer {
+    /** CLI verbose flag. */
+    private final boolean verbose;
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Logger.
+     */
+    private static final Logger log = GridIntListMigrator.GetMigratorLogger();
+
+    /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+    private final Constructor<?> newKeysCachedConstruct;
+
+    Transformer(boolean verb) {
+        verbose = verb;
+        newKeysCachedConstruct = findNewKeysConstruct();
+    }
+
+    /**
+     * Recursively transforms a value.
+     * <p>
+     * Rules:
+     * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+     * - Java object of OLD_KEYS_TYPE -> same replacement.
+     * - BinaryObject (other type) -> rebuild only if some field changed.
+     * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+     * - Primitives/String/int[] -> unchanged.
+     *
+     * @param v     value to transform
+     * @param depth recursion depth (fixed guard inside)
+     * @return transform result with possibly new value and 'changed' flag
+     */
+    TransformResult transform(Object v, int depth) {
+        if (v == null)
+            return TransformResult.same(null);
+
+        // Strict migration interruption
+        if (depth > MAX_DEPTH) {
+            String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+            throw new IllegalStateException(errMsg);
+        }
+
+        // Binary legacy type
+        if (v instanceof BinaryObject) {
+            BinaryObject bo = (BinaryObject)v;
+            String typeName = bo.type().typeName();
+
+            if (typeName.equals(OLD_KEYS_TYPE)) {
+                int[] ints = extractInts(bo);
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Generic BinaryObject
+            BinaryObjectBuilder bb = bo.toBuilder();
+            boolean anyChanged = false;
+
+            for (String oldChildField : bo.type().fieldNames()) {
+                // Transform nested fields
+                TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                if (childRes.changed) {
+                    bb.setField(oldChildField, childRes.val);
+
+                    anyChanged = true;
+                }
+            }
+
+            // Rebuild only if some field changed
+            return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+        }
+
+        // Java legacy type
+        if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+            org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+            int[] ints = new int[g.size()];
+
+            for (int i = 0; i < ints.length; i++)
+                ints[i] = g.get(i);
+
+            Object newKeys = buildNewKeys(ints);
+
+            return TransformResult.changed(newKeys);
+        }
+
+        // Already new type
+        if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+            return TransformResult.same(v);
+
+        // Collections
+        if (v instanceof List) {
+            List<?> src = (List<?>)v;
+            boolean anyChanged = false;
+
+            List<Object> out = new ArrayList<>(src.size());
+
+            for (Object listElem : src) {
+                TransformResult tr = transform(listElem, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Set) {
+            Set<?> src = (Set<?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Object el : src) {
+                TransformResult tr = transform(el, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Map) {
+            Map<?, ?> src = (Map<?, ?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Map.Entry<?, ?> en : src.entrySet()) {
+                TransformResult k = transform(en.getKey(), depth + 1);
+                TransformResult val = transform(en.getValue(), depth + 1);
+
+                out.put(k.val, val.val);
+                anyChanged |= k.changed || val.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Object[] arrays (non-primitive)
+        if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+            Object[] arr = (Object[])v;
+            Object[] out = new Object[arr.length];
+
+            boolean anyChanged = false;
+
+            for (int i = 0; i < arr.length; i++) {
+                TransformResult tr = transform(arr[i], depth + 1);
+
+                out[i] = tr.val;
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Primitives, String, int[] etc.: unchanged
+        return TransformResult.same(v);
+    }
+
+    /**
+     * Extracts int[] from legacy GridIntList BinaryObject.
+     */
+    private int[] extractInts(BinaryObject oldKeys) {
+        try {
+            Object obj = oldKeys.deserialize();
+
+            if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                int[] res = new int[g.size()];
+
+                for (int i = 0; i < res.length; i++)
+                    res[i] = g.get(i);
+
+                return res;
+            }
+        }
+        catch (Throwable t) {
+            if (verbose)
+                log.info("Deserialize fallback: {}", t.toString());

Review Comment:
   At which case we failed 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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315486776


##########
migrator/src/main/java/migrate/Transformer.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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 src.main.java.migrate;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+ * Guarded by a MAX_DEPTH to avoid accidental cycles.
+ */
+public final class Transformer {
+    /** CLI verbose flag. */
+    private final boolean verbose;
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Logger.
+     */
+    private static final Logger log = GridIntListMigrator.GetMigratorLogger();
+
+    /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+    private final Constructor<?> newKeysCachedConstruct;
+
+    Transformer(boolean verb) {
+        verbose = verb;
+        newKeysCachedConstruct = findNewKeysConstruct();
+    }
+
+    /**
+     * Recursively transforms a value.
+     * <p>
+     * Rules:
+     * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+     * - Java object of OLD_KEYS_TYPE -> same replacement.
+     * - BinaryObject (other type) -> rebuild only if some field changed.
+     * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+     * - Primitives/String/int[] -> unchanged.
+     *
+     * @param v     value to transform
+     * @param depth recursion depth (fixed guard inside)
+     * @return transform result with possibly new value and 'changed' flag
+     */
+    TransformResult transform(Object v, int depth) {
+        if (v == null)
+            return TransformResult.same(null);
+
+        // Strict migration interruption
+        if (depth > MAX_DEPTH) {
+            String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+            throw new IllegalStateException(errMsg);
+        }
+
+        // Binary legacy type
+        if (v instanceof BinaryObject) {
+            BinaryObject bo = (BinaryObject)v;
+            String typeName = bo.type().typeName();
+
+            if (typeName.equals(OLD_KEYS_TYPE)) {
+                int[] ints = extractInts(bo);
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Generic BinaryObject
+            BinaryObjectBuilder bb = bo.toBuilder();
+            boolean anyChanged = false;
+
+            for (String oldChildField : bo.type().fieldNames()) {
+                // Transform nested fields
+                TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                if (childRes.changed) {
+                    bb.setField(oldChildField, childRes.val);
+
+                    anyChanged = true;
+                }
+            }
+
+            // Rebuild only if some field changed
+            return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+        }
+
+        // Java legacy type
+        if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+            org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+            int[] ints = new int[g.size()];
+
+            for (int i = 0; i < ints.length; i++)
+                ints[i] = g.get(i);
+
+            Object newKeys = buildNewKeys(ints);
+
+            return TransformResult.changed(newKeys);
+        }
+
+        // Already new type
+        if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+            return TransformResult.same(v);
+
+        // Collections
+        if (v instanceof List) {
+            List<?> src = (List<?>)v;
+            boolean anyChanged = false;
+
+            List<Object> out = new ArrayList<>(src.size());
+
+            for (Object listElem : src) {
+                TransformResult tr = transform(listElem, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Set) {
+            Set<?> src = (Set<?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Object el : src) {
+                TransformResult tr = transform(el, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Map) {
+            Map<?, ?> src = (Map<?, ?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Map.Entry<?, ?> en : src.entrySet()) {
+                TransformResult k = transform(en.getKey(), depth + 1);
+                TransformResult val = transform(en.getValue(), depth + 1);
+
+                out.put(k.val, val.val);
+                anyChanged |= k.changed || val.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Object[] arrays (non-primitive)
+        if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+            Object[] arr = (Object[])v;
+            Object[] out = new Object[arr.length];
+
+            boolean anyChanged = false;
+
+            for (int i = 0; i < arr.length; i++) {
+                TransformResult tr = transform(arr[i], depth + 1);
+
+                out[i] = tr.val;
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Primitives, String, int[] etc.: unchanged
+        return TransformResult.same(v);
+    }
+
+    /**
+     * Extracts int[] from legacy GridIntList BinaryObject.
+     */
+    private int[] extractInts(BinaryObject oldKeys) {
+        try {
+            Object obj = oldKeys.deserialize();
+
+            if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                int[] res = new int[g.size()];
+
+                for (int i = 0; i < res.length; i++)
+                    res[i] = g.get(i);
+
+                return res;
+            }
+        }
+        catch (Throwable t) {
+            if (verbose)
+                log.info("Deserialize fallback: {}", t.toString());
+        }
+
+        // Hardcode ("arr") based on GridIntList fields
+        Collection<String> childFields = oldKeys.type().fieldNames();
+        if (childFields.contains("arr")) {
+            int[] arr = oldKeys.field("arr");
+
+            if (arr != null)
+                return arr;
+        }
+
+        log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+        return new int[0]; // best effort fallback
+    }
+
+    /**
+     * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+     * or falls back to int[] if the class is not on the classpath.
+     */
+    private Object buildNewKeys(int[] ints) {
+        if (newKeysCachedConstruct != null) {

Review Comment:
   At which case it is null?



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2301620303


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());
+
+            if (a.cacheFilter != null && !a.cacheFilter.isEmpty())
+                cacheNames.removeIf(n -> !n.contains(a.cacheFilter));
+
+            log.info("Caches to scan: {}", cacheNames);
+
+            Transformer transformer = new Transformer(a.verbose);
+
+            for (String cacheName : cacheNames) {
+                IgniteCache<Object, Object> rawCache = ig.cache(cacheName);
+
+                if (rawCache == null)
+                    continue;
+
+                IgniteCache<Object, Object> c = rawCache.withKeepBinary();
+
+                log.info("Scanning cache: {}", cacheName);
+
+                ScanQuery<Object, Object> q = new ScanQuery<>();
+                q.setPageSize(DEFAULT_PAGE_SIZE);
+
+                AtomicLong scanned = new AtomicLong();
+                AtomicLong updated = new AtomicLong();
+
+                try (QueryCursor<Cache.Entry<Object, Object>> cur = c.query(q)) {
+                    for (Cache.Entry<Object, Object> e : cur) {
+                        try {
+                            Object v = e.getValue();
+                            TransformResult tr = transformer.transform(v, 0);
+
+                            if (tr.changed) {
+                                if (a.apply) {
+                                    c.put(e.getKey(), tr.val);
+
+                                    updated.incrementAndGet();
+                                }
+                                else if (a.verbose)
+                                    log.info("DRY-RUN would update key={}", e.getKey());
+                            }
+
+                            long s = scanned.incrementAndGet();
+
+                            if (s % a.reportEvery == 0)
+                                log.info("Scanned={} updated={}", s, updated.get());
+                        }
+                        catch (Throwable t) {
+                            log.warn("Entry migration failed, skipping. Cause: {}", t.toString());
+
+                            scanned.incrementAndGet();
+                        }
+                    }
+                }
+
+                log.info("Done {}: scanned={} updated={}", cacheName, scanned.get(), updated.get());
+            }
+
+            log.info("Migration finished.");
+        }
+    }
+
+    /**
+     * Infers consistentId by reading first subdirectory under work/db.
+     *
+     * @param workDir path to Ignite work dir.
+     * @return consistentId or null if not found.
+     */
+    private static String detectConsistentId(String workDir) {
+        File dbDir = new File(workDir, "db");
+
+        if (!dbDir.isDirectory())
+            return null;
+
+        File[] kids = dbDir.listFiles(File::isDirectory);
+
+        if (kids == null || kids.length == 0)
+            return null;
+
+        return kids[0].getName();
+    }
+
+    /**
+     * Result of a recursive transformation: value and flag indicating changes.
+     */
+    private static final class TransformResult {
+        /** Value. */
+        final Object val;
+
+        /** Changed. */
+        final boolean changed;
+
+        private TransformResult(Object val, boolean changed) {
+            this.val = val;
+            this.changed = changed;
+        }
+
+        static TransformResult same(Object v) {
+            return new TransformResult(v, false);
+        }
+
+        static TransformResult changed(Object v) {
+            return new TransformResult(v, true);
+        }
+    }
+
+    /**
+     * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+     * Guarded by a MAX_DEPTH to avoid accidental cycles.
+     */
+    private static final class Transformer {
+        private final boolean verbose;
+
+        /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+        private final Constructor<?> newKeysCachedConstruct;
+
+        Transformer(boolean verb) {
+            verbose = verb;
+            newKeysCachedConstruct = findNewKeysConstruct();
+        }
+
+        /**
+         * Recursively transforms a value.
+         * <p>
+         * Rules:
+         * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+         * - Java object of OLD_KEYS_TYPE -> same replacement.
+         * - BinaryObject (other type) -> rebuild only if some field changed.
+         * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+         * - Primitives/String/int[] -> unchanged.
+         *
+         * @param v     value to transform
+         * @param depth recursion depth (fixed guard inside)
+         * @return transform result with possibly new value and 'changed' flag
+         */
+        TransformResult transform(Object v, int depth) {
+            if (v == null)
+                return TransformResult.same(null);
+
+            // Strict migration interruption
+            if (depth > MAX_DEPTH) {
+                String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+                throw new IllegalStateException(errMsg);
+            }
+
+            // Binary legacy type
+            if (v instanceof BinaryObject) {
+                BinaryObject bo = (BinaryObject)v;
+                String typeName = bo.type().typeName();
+
+                if (typeName.equals(OLD_KEYS_TYPE)) {
+                    int[] ints = extractInts(bo);
+                    Object newKeys = buildNewKeys(ints);
+
+                    return TransformResult.changed(newKeys);
+                }
+
+                // Generic BinaryObject
+                BinaryObjectBuilder bb = bo.toBuilder();
+                boolean anyChanged = false;
+
+                for (String oldChildField : bo.type().fieldNames()) {
+                    // Transform nested fields
+                    TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                    if (childRes.changed) {
+                        bb.setField(oldChildField, childRes.val);
+
+                        anyChanged = true;
+                    }
+                }
+
+                // Rebuild only if some field changed
+                return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+            }
+
+            // Java legacy type
+            if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+                int[] ints = new int[g.size()];
+
+                for (int i = 0; i < ints.length; i++)
+                    ints[i] = g.get(i);
+
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Already new type
+            if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+                return TransformResult.same(v);
+
+            // Collections
+            if (v instanceof List) {
+                List<?> src = (List<?>)v;
+                boolean anyChanged = false;
+
+                List<Object> out = new ArrayList<>(src.size());
+
+                for (Object listElem : src) {
+                    TransformResult tr = transform(listElem, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Set) {
+                Set<?> src = (Set<?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Object el : src) {
+                    TransformResult tr = transform(el, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Map) {
+                Map<?, ?> src = (Map<?, ?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Map.Entry<?, ?> en : src.entrySet()) {
+                    TransformResult k = transform(en.getKey(), depth + 1);
+                    TransformResult val = transform(en.getValue(), depth + 1);
+
+                    out.put(k.val, val.val);
+                    anyChanged |= k.changed || val.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Object[] arrays (non-primitive)
+            if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+                Object[] arr = (Object[])v;
+                Object[] out = new Object[arr.length];
+
+                boolean anyChanged = false;
+
+                for (int i = 0; i < arr.length; i++) {
+                    TransformResult tr = transform(arr[i], depth + 1);
+
+                    out[i] = tr.val;
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Primitives, String, int[] etc.: unchanged
+            return TransformResult.same(v);
+        }
+
+        /**
+         * Extracts int[] from legacy GridIntList BinaryObject.
+         */
+        private int[] extractInts(BinaryObject oldKeys) {
+            try {
+                Object obj = oldKeys.deserialize();
+
+                if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                    org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                    int[] res = new int[g.size()];
+
+                    for (int i = 0; i < res.length; i++)
+                        res[i] = g.get(i);
+
+                    return res;
+                }
+            }
+            catch (Throwable t) {
+                if (verbose)
+                    log.info("Deserialize fallback: {}", t.toString());
+            }
+
+            // Hardcode ("arr") based on GridIntList fields
+            Collection<String> childFields = oldKeys.type().fieldNames();
+            if (childFields.contains("arr")) {
+                int[] arr = oldKeys.field("arr");
+
+                if (arr != null)
+                    return arr;
+            }
+
+            log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+            return new int[0]; // best effort fallback
+        }
+
+        /**
+         * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+         * or falls back to int[] if the class is not on the classpath.
+         */
+        private Object buildNewKeys(int[] ints) {
+            if (newKeysCachedConstruct != null) {
+                try {
+                    return newKeysCachedConstruct.newInstance((Object)ints);
+                }
+                catch (ReflectiveOperationException ignored) {
+                    // fall through to fallback
+                }
+            }
+
+            if (verbose)
+                log.warn("NEW_KEYS_TYPE {} is not available; falling back to raw int[]", NEW_KEYS_TYPE);
+
+            return ints; // best effort fallback
+        }
+
+        /**
+         * Resolves constructor NEW_KEYS_TYPE(int[]) once to avoid reflection per entry.
+         */
+        private Constructor<?> findNewKeysConstruct() {
+            try {
+                Class<?> cls = Class.forName(NEW_KEYS_TYPE);
+
+                return cls.getConstructor(int[].class);
+            }
+            catch (Throwable t) {
+                if (verbose) {
+                    log.warn("New keys type {} is not on classpath, will fallback to int[] (cause: {})",

Review Comment:
   Is it better to fail entire build? Looks like in that case we can load third party constructor to proceed migration.
   It seems logical to add constructor != null check to avoid npe



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2301620303


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());
+
+            if (a.cacheFilter != null && !a.cacheFilter.isEmpty())
+                cacheNames.removeIf(n -> !n.contains(a.cacheFilter));
+
+            log.info("Caches to scan: {}", cacheNames);
+
+            Transformer transformer = new Transformer(a.verbose);
+
+            for (String cacheName : cacheNames) {
+                IgniteCache<Object, Object> rawCache = ig.cache(cacheName);
+
+                if (rawCache == null)
+                    continue;
+
+                IgniteCache<Object, Object> c = rawCache.withKeepBinary();
+
+                log.info("Scanning cache: {}", cacheName);
+
+                ScanQuery<Object, Object> q = new ScanQuery<>();
+                q.setPageSize(DEFAULT_PAGE_SIZE);
+
+                AtomicLong scanned = new AtomicLong();
+                AtomicLong updated = new AtomicLong();
+
+                try (QueryCursor<Cache.Entry<Object, Object>> cur = c.query(q)) {
+                    for (Cache.Entry<Object, Object> e : cur) {
+                        try {
+                            Object v = e.getValue();
+                            TransformResult tr = transformer.transform(v, 0);
+
+                            if (tr.changed) {
+                                if (a.apply) {
+                                    c.put(e.getKey(), tr.val);
+
+                                    updated.incrementAndGet();
+                                }
+                                else if (a.verbose)
+                                    log.info("DRY-RUN would update key={}", e.getKey());
+                            }
+
+                            long s = scanned.incrementAndGet();
+
+                            if (s % a.reportEvery == 0)
+                                log.info("Scanned={} updated={}", s, updated.get());
+                        }
+                        catch (Throwable t) {
+                            log.warn("Entry migration failed, skipping. Cause: {}", t.toString());
+
+                            scanned.incrementAndGet();
+                        }
+                    }
+                }
+
+                log.info("Done {}: scanned={} updated={}", cacheName, scanned.get(), updated.get());
+            }
+
+            log.info("Migration finished.");
+        }
+    }
+
+    /**
+     * Infers consistentId by reading first subdirectory under work/db.
+     *
+     * @param workDir path to Ignite work dir.
+     * @return consistentId or null if not found.
+     */
+    private static String detectConsistentId(String workDir) {
+        File dbDir = new File(workDir, "db");
+
+        if (!dbDir.isDirectory())
+            return null;
+
+        File[] kids = dbDir.listFiles(File::isDirectory);
+
+        if (kids == null || kids.length == 0)
+            return null;
+
+        return kids[0].getName();
+    }
+
+    /**
+     * Result of a recursive transformation: value and flag indicating changes.
+     */
+    private static final class TransformResult {
+        /** Value. */
+        final Object val;
+
+        /** Changed. */
+        final boolean changed;
+
+        private TransformResult(Object val, boolean changed) {
+            this.val = val;
+            this.changed = changed;
+        }
+
+        static TransformResult same(Object v) {
+            return new TransformResult(v, false);
+        }
+
+        static TransformResult changed(Object v) {
+            return new TransformResult(v, true);
+        }
+    }
+
+    /**
+     * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+     * Guarded by a MAX_DEPTH to avoid accidental cycles.
+     */
+    private static final class Transformer {
+        private final boolean verbose;
+
+        /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+        private final Constructor<?> newKeysCachedConstruct;
+
+        Transformer(boolean verb) {
+            verbose = verb;
+            newKeysCachedConstruct = findNewKeysConstruct();
+        }
+
+        /**
+         * Recursively transforms a value.
+         * <p>
+         * Rules:
+         * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+         * - Java object of OLD_KEYS_TYPE -> same replacement.
+         * - BinaryObject (other type) -> rebuild only if some field changed.
+         * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+         * - Primitives/String/int[] -> unchanged.
+         *
+         * @param v     value to transform
+         * @param depth recursion depth (fixed guard inside)
+         * @return transform result with possibly new value and 'changed' flag
+         */
+        TransformResult transform(Object v, int depth) {
+            if (v == null)
+                return TransformResult.same(null);
+
+            // Strict migration interruption
+            if (depth > MAX_DEPTH) {
+                String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+                throw new IllegalStateException(errMsg);
+            }
+
+            // Binary legacy type
+            if (v instanceof BinaryObject) {
+                BinaryObject bo = (BinaryObject)v;
+                String typeName = bo.type().typeName();
+
+                if (typeName.equals(OLD_KEYS_TYPE)) {
+                    int[] ints = extractInts(bo);
+                    Object newKeys = buildNewKeys(ints);
+
+                    return TransformResult.changed(newKeys);
+                }
+
+                // Generic BinaryObject
+                BinaryObjectBuilder bb = bo.toBuilder();
+                boolean anyChanged = false;
+
+                for (String oldChildField : bo.type().fieldNames()) {
+                    // Transform nested fields
+                    TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                    if (childRes.changed) {
+                        bb.setField(oldChildField, childRes.val);
+
+                        anyChanged = true;
+                    }
+                }
+
+                // Rebuild only if some field changed
+                return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+            }
+
+            // Java legacy type
+            if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+                int[] ints = new int[g.size()];
+
+                for (int i = 0; i < ints.length; i++)
+                    ints[i] = g.get(i);
+
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Already new type
+            if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+                return TransformResult.same(v);
+
+            // Collections
+            if (v instanceof List) {
+                List<?> src = (List<?>)v;
+                boolean anyChanged = false;
+
+                List<Object> out = new ArrayList<>(src.size());
+
+                for (Object listElem : src) {
+                    TransformResult tr = transform(listElem, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Set) {
+                Set<?> src = (Set<?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Object el : src) {
+                    TransformResult tr = transform(el, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Map) {
+                Map<?, ?> src = (Map<?, ?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Map.Entry<?, ?> en : src.entrySet()) {
+                    TransformResult k = transform(en.getKey(), depth + 1);
+                    TransformResult val = transform(en.getValue(), depth + 1);
+
+                    out.put(k.val, val.val);
+                    anyChanged |= k.changed || val.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Object[] arrays (non-primitive)
+            if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+                Object[] arr = (Object[])v;
+                Object[] out = new Object[arr.length];
+
+                boolean anyChanged = false;
+
+                for (int i = 0; i < arr.length; i++) {
+                    TransformResult tr = transform(arr[i], depth + 1);
+
+                    out[i] = tr.val;
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Primitives, String, int[] etc.: unchanged
+            return TransformResult.same(v);
+        }
+
+        /**
+         * Extracts int[] from legacy GridIntList BinaryObject.
+         */
+        private int[] extractInts(BinaryObject oldKeys) {
+            try {
+                Object obj = oldKeys.deserialize();
+
+                if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                    org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                    int[] res = new int[g.size()];
+
+                    for (int i = 0; i < res.length; i++)
+                        res[i] = g.get(i);
+
+                    return res;
+                }
+            }
+            catch (Throwable t) {
+                if (verbose)
+                    log.info("Deserialize fallback: {}", t.toString());
+            }
+
+            // Hardcode ("arr") based on GridIntList fields
+            Collection<String> childFields = oldKeys.type().fieldNames();
+            if (childFields.contains("arr")) {
+                int[] arr = oldKeys.field("arr");
+
+                if (arr != null)
+                    return arr;
+            }
+
+            log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+            return new int[0]; // best effort fallback
+        }
+
+        /**
+         * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+         * or falls back to int[] if the class is not on the classpath.
+         */
+        private Object buildNewKeys(int[] ints) {
+            if (newKeysCachedConstruct != null) {
+                try {
+                    return newKeysCachedConstruct.newInstance((Object)ints);
+                }
+                catch (ReflectiveOperationException ignored) {
+                    // fall through to fallback
+                }
+            }
+
+            if (verbose)
+                log.warn("NEW_KEYS_TYPE {} is not available; falling back to raw int[]", NEW_KEYS_TYPE);
+
+            return ints; // best effort fallback
+        }
+
+        /**
+         * Resolves constructor NEW_KEYS_TYPE(int[]) once to avoid reflection per entry.
+         */
+        private Constructor<?> findNewKeysConstruct() {
+            try {
+                Class<?> cls = Class.forName(NEW_KEYS_TYPE);
+
+                return cls.getConstructor(int[].class);
+            }
+            catch (Throwable t) {
+                if (verbose) {
+                    log.warn("New keys type {} is not on classpath, will fallback to int[] (cause: {})",

Review Comment:
   Is it better to fail entire build? Looks like in that case we can load third party constructor to proceed migration.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2301620303


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());
+
+            if (a.cacheFilter != null && !a.cacheFilter.isEmpty())
+                cacheNames.removeIf(n -> !n.contains(a.cacheFilter));
+
+            log.info("Caches to scan: {}", cacheNames);
+
+            Transformer transformer = new Transformer(a.verbose);
+
+            for (String cacheName : cacheNames) {
+                IgniteCache<Object, Object> rawCache = ig.cache(cacheName);
+
+                if (rawCache == null)
+                    continue;
+
+                IgniteCache<Object, Object> c = rawCache.withKeepBinary();
+
+                log.info("Scanning cache: {}", cacheName);
+
+                ScanQuery<Object, Object> q = new ScanQuery<>();
+                q.setPageSize(DEFAULT_PAGE_SIZE);
+
+                AtomicLong scanned = new AtomicLong();
+                AtomicLong updated = new AtomicLong();
+
+                try (QueryCursor<Cache.Entry<Object, Object>> cur = c.query(q)) {
+                    for (Cache.Entry<Object, Object> e : cur) {
+                        try {
+                            Object v = e.getValue();
+                            TransformResult tr = transformer.transform(v, 0);
+
+                            if (tr.changed) {
+                                if (a.apply) {
+                                    c.put(e.getKey(), tr.val);
+
+                                    updated.incrementAndGet();
+                                }
+                                else if (a.verbose)
+                                    log.info("DRY-RUN would update key={}", e.getKey());
+                            }
+
+                            long s = scanned.incrementAndGet();
+
+                            if (s % a.reportEvery == 0)
+                                log.info("Scanned={} updated={}", s, updated.get());
+                        }
+                        catch (Throwable t) {
+                            log.warn("Entry migration failed, skipping. Cause: {}", t.toString());
+
+                            scanned.incrementAndGet();
+                        }
+                    }
+                }
+
+                log.info("Done {}: scanned={} updated={}", cacheName, scanned.get(), updated.get());
+            }
+
+            log.info("Migration finished.");
+        }
+    }
+
+    /**
+     * Infers consistentId by reading first subdirectory under work/db.
+     *
+     * @param workDir path to Ignite work dir.
+     * @return consistentId or null if not found.
+     */
+    private static String detectConsistentId(String workDir) {
+        File dbDir = new File(workDir, "db");
+
+        if (!dbDir.isDirectory())
+            return null;
+
+        File[] kids = dbDir.listFiles(File::isDirectory);
+
+        if (kids == null || kids.length == 0)
+            return null;
+
+        return kids[0].getName();
+    }
+
+    /**
+     * Result of a recursive transformation: value and flag indicating changes.
+     */
+    private static final class TransformResult {
+        /** Value. */
+        final Object val;
+
+        /** Changed. */
+        final boolean changed;
+
+        private TransformResult(Object val, boolean changed) {
+            this.val = val;
+            this.changed = changed;
+        }
+
+        static TransformResult same(Object v) {
+            return new TransformResult(v, false);
+        }
+
+        static TransformResult changed(Object v) {
+            return new TransformResult(v, true);
+        }
+    }
+
+    /**
+     * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+     * Guarded by a MAX_DEPTH to avoid accidental cycles.
+     */
+    private static final class Transformer {
+        private final boolean verbose;
+
+        /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+        private final Constructor<?> newKeysCachedConstruct;
+
+        Transformer(boolean verb) {
+            verbose = verb;
+            newKeysCachedConstruct = findNewKeysConstruct();
+        }
+
+        /**
+         * Recursively transforms a value.
+         * <p>
+         * Rules:
+         * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+         * - Java object of OLD_KEYS_TYPE -> same replacement.
+         * - BinaryObject (other type) -> rebuild only if some field changed.
+         * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+         * - Primitives/String/int[] -> unchanged.
+         *
+         * @param v     value to transform
+         * @param depth recursion depth (fixed guard inside)
+         * @return transform result with possibly new value and 'changed' flag
+         */
+        TransformResult transform(Object v, int depth) {
+            if (v == null)
+                return TransformResult.same(null);
+
+            // Strict migration interruption
+            if (depth > MAX_DEPTH) {
+                String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+                throw new IllegalStateException(errMsg);
+            }
+
+            // Binary legacy type
+            if (v instanceof BinaryObject) {
+                BinaryObject bo = (BinaryObject)v;
+                String typeName = bo.type().typeName();
+
+                if (typeName.equals(OLD_KEYS_TYPE)) {
+                    int[] ints = extractInts(bo);
+                    Object newKeys = buildNewKeys(ints);
+
+                    return TransformResult.changed(newKeys);
+                }
+
+                // Generic BinaryObject
+                BinaryObjectBuilder bb = bo.toBuilder();
+                boolean anyChanged = false;
+
+                for (String oldChildField : bo.type().fieldNames()) {
+                    // Transform nested fields
+                    TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                    if (childRes.changed) {
+                        bb.setField(oldChildField, childRes.val);
+
+                        anyChanged = true;
+                    }
+                }
+
+                // Rebuild only if some field changed
+                return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+            }
+
+            // Java legacy type
+            if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+                int[] ints = new int[g.size()];
+
+                for (int i = 0; i < ints.length; i++)
+                    ints[i] = g.get(i);
+
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Already new type
+            if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+                return TransformResult.same(v);
+
+            // Collections
+            if (v instanceof List) {
+                List<?> src = (List<?>)v;
+                boolean anyChanged = false;
+
+                List<Object> out = new ArrayList<>(src.size());
+
+                for (Object listElem : src) {
+                    TransformResult tr = transform(listElem, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Set) {
+                Set<?> src = (Set<?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Object el : src) {
+                    TransformResult tr = transform(el, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Map) {
+                Map<?, ?> src = (Map<?, ?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Map.Entry<?, ?> en : src.entrySet()) {
+                    TransformResult k = transform(en.getKey(), depth + 1);
+                    TransformResult val = transform(en.getValue(), depth + 1);
+
+                    out.put(k.val, val.val);
+                    anyChanged |= k.changed || val.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Object[] arrays (non-primitive)
+            if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+                Object[] arr = (Object[])v;
+                Object[] out = new Object[arr.length];
+
+                boolean anyChanged = false;
+
+                for (int i = 0; i < arr.length; i++) {
+                    TransformResult tr = transform(arr[i], depth + 1);
+
+                    out[i] = tr.val;
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Primitives, String, int[] etc.: unchanged
+            return TransformResult.same(v);
+        }
+
+        /**
+         * Extracts int[] from legacy GridIntList BinaryObject.
+         */
+        private int[] extractInts(BinaryObject oldKeys) {
+            try {
+                Object obj = oldKeys.deserialize();
+
+                if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                    org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                    int[] res = new int[g.size()];
+
+                    for (int i = 0; i < res.length; i++)
+                        res[i] = g.get(i);
+
+                    return res;
+                }
+            }
+            catch (Throwable t) {
+                if (verbose)
+                    log.info("Deserialize fallback: {}", t.toString());
+            }
+
+            // Hardcode ("arr") based on GridIntList fields
+            Collection<String> childFields = oldKeys.type().fieldNames();
+            if (childFields.contains("arr")) {
+                int[] arr = oldKeys.field("arr");
+
+                if (arr != null)
+                    return arr;
+            }
+
+            log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+            return new int[0]; // best effort fallback
+        }
+
+        /**
+         * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+         * or falls back to int[] if the class is not on the classpath.
+         */
+        private Object buildNewKeys(int[] ints) {
+            if (newKeysCachedConstruct != null) {
+                try {
+                    return newKeysCachedConstruct.newInstance((Object)ints);
+                }
+                catch (ReflectiveOperationException ignored) {
+                    // fall through to fallback
+                }
+            }
+
+            if (verbose)
+                log.warn("NEW_KEYS_TYPE {} is not available; falling back to raw int[]", NEW_KEYS_TYPE);
+
+            return ints; // best effort fallback
+        }
+
+        /**
+         * Resolves constructor NEW_KEYS_TYPE(int[]) once to avoid reflection per entry.
+         */
+        private Constructor<?> findNewKeysConstruct() {
+            try {
+                Class<?> cls = Class.forName(NEW_KEYS_TYPE);
+
+                return cls.getConstructor(int[].class);
+            }
+            catch (Throwable t) {
+                if (verbose) {
+                    log.warn("New keys type {} is not on classpath, will fallback to int[] (cause: {})",

Review Comment:
   Is it better to fail entire build? Looks like in that case we can load third party constructor to proceed migration. Anyway it seems logical to add constructor != null check to avoid npe



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315476633


##########
migrator/src/main/java/migrate/Transformer.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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 src.main.java.migrate;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+ * Guarded by a MAX_DEPTH to avoid accidental cycles.
+ */
+public final class Transformer {
+    /** CLI verbose flag. */
+    private final boolean verbose;
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Logger.
+     */
+    private static final Logger log = GridIntListMigrator.GetMigratorLogger();
+
+    /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+    private final Constructor<?> newKeysCachedConstruct;
+
+    Transformer(boolean verb) {
+        verbose = verb;
+        newKeysCachedConstruct = findNewKeysConstruct();
+    }
+
+    /**
+     * Recursively transforms a value.
+     * <p>
+     * Rules:
+     * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+     * - Java object of OLD_KEYS_TYPE -> same replacement.
+     * - BinaryObject (other type) -> rebuild only if some field changed.
+     * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+     * - Primitives/String/int[] -> unchanged.
+     *
+     * @param v     value to transform
+     * @param depth recursion depth (fixed guard inside)
+     * @return transform result with possibly new value and 'changed' flag
+     */
+    TransformResult transform(Object v, int depth) {
+        if (v == null)
+            return TransformResult.same(null);
+
+        // Strict migration interruption
+        if (depth > MAX_DEPTH) {
+            String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+            throw new IllegalStateException(errMsg);
+        }
+
+        // Binary legacy type
+        if (v instanceof BinaryObject) {
+            BinaryObject bo = (BinaryObject)v;
+            String typeName = bo.type().typeName();
+
+            if (typeName.equals(OLD_KEYS_TYPE)) {
+                int[] ints = extractInts(bo);
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Generic BinaryObject
+            BinaryObjectBuilder bb = bo.toBuilder();
+            boolean anyChanged = false;
+
+            for (String oldChildField : bo.type().fieldNames()) {
+                // Transform nested fields
+                TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                if (childRes.changed) {
+                    bb.setField(oldChildField, childRes.val);
+
+                    anyChanged = true;
+                }
+            }
+
+            // Rebuild only if some field changed
+            return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+        }
+
+        // Java legacy type
+        if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+            org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+            int[] ints = new int[g.size()];
+
+            for (int i = 0; i < ints.length; i++)
+                ints[i] = g.get(i);
+
+            Object newKeys = buildNewKeys(ints);
+
+            return TransformResult.changed(newKeys);
+        }
+
+        // Already new type
+        if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+            return TransformResult.same(v);
+
+        // Collections
+        if (v instanceof List) {
+            List<?> src = (List<?>)v;
+            boolean anyChanged = false;
+
+            List<Object> out = new ArrayList<>(src.size());
+
+            for (Object listElem : src) {
+                TransformResult tr = transform(listElem, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Set) {
+            Set<?> src = (Set<?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Object el : src) {
+                TransformResult tr = transform(el, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Map) {
+            Map<?, ?> src = (Map<?, ?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Map.Entry<?, ?> en : src.entrySet()) {
+                TransformResult k = transform(en.getKey(), depth + 1);
+                TransformResult val = transform(en.getValue(), depth + 1);
+
+                out.put(k.val, val.val);
+                anyChanged |= k.changed || val.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Object[] arrays (non-primitive)
+        if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+            Object[] arr = (Object[])v;
+            Object[] out = new Object[arr.length];
+
+            boolean anyChanged = false;
+
+            for (int i = 0; i < arr.length; i++) {
+                TransformResult tr = transform(arr[i], depth + 1);
+
+                out[i] = tr.val;
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Primitives, String, int[] etc.: unchanged
+        return TransformResult.same(v);
+    }
+
+    /**
+     * Extracts int[] from legacy GridIntList BinaryObject.
+     */
+    private int[] extractInts(BinaryObject oldKeys) {
+        try {
+            Object obj = oldKeys.deserialize();
+
+            if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                int[] res = new int[g.size()];

Review Comment:
   Let's do not create new array for every record.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315419041


##########
migrator/src/main/java/migrate/TransformResult.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * 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 src.main.java.migrate;

Review Comment:
   wrong package. 



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315486776


##########
migrator/src/main/java/migrate/Transformer.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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 src.main.java.migrate;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+ * Guarded by a MAX_DEPTH to avoid accidental cycles.
+ */
+public final class Transformer {
+    /** CLI verbose flag. */
+    private final boolean verbose;
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Logger.
+     */
+    private static final Logger log = GridIntListMigrator.GetMigratorLogger();
+
+    /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+    private final Constructor<?> newKeysCachedConstruct;
+
+    Transformer(boolean verb) {
+        verbose = verb;
+        newKeysCachedConstruct = findNewKeysConstruct();
+    }
+
+    /**
+     * Recursively transforms a value.
+     * <p>
+     * Rules:
+     * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+     * - Java object of OLD_KEYS_TYPE -> same replacement.
+     * - BinaryObject (other type) -> rebuild only if some field changed.
+     * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+     * - Primitives/String/int[] -> unchanged.
+     *
+     * @param v     value to transform
+     * @param depth recursion depth (fixed guard inside)
+     * @return transform result with possibly new value and 'changed' flag
+     */
+    TransformResult transform(Object v, int depth) {
+        if (v == null)
+            return TransformResult.same(null);
+
+        // Strict migration interruption
+        if (depth > MAX_DEPTH) {
+            String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+            throw new IllegalStateException(errMsg);
+        }
+
+        // Binary legacy type
+        if (v instanceof BinaryObject) {
+            BinaryObject bo = (BinaryObject)v;
+            String typeName = bo.type().typeName();
+
+            if (typeName.equals(OLD_KEYS_TYPE)) {
+                int[] ints = extractInts(bo);
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Generic BinaryObject
+            BinaryObjectBuilder bb = bo.toBuilder();
+            boolean anyChanged = false;
+
+            for (String oldChildField : bo.type().fieldNames()) {
+                // Transform nested fields
+                TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                if (childRes.changed) {
+                    bb.setField(oldChildField, childRes.val);
+
+                    anyChanged = true;
+                }
+            }
+
+            // Rebuild only if some field changed
+            return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+        }
+
+        // Java legacy type
+        if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+            org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+            int[] ints = new int[g.size()];
+
+            for (int i = 0; i < ints.length; i++)
+                ints[i] = g.get(i);
+
+            Object newKeys = buildNewKeys(ints);
+
+            return TransformResult.changed(newKeys);
+        }
+
+        // Already new type
+        if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+            return TransformResult.same(v);
+
+        // Collections
+        if (v instanceof List) {
+            List<?> src = (List<?>)v;
+            boolean anyChanged = false;
+
+            List<Object> out = new ArrayList<>(src.size());
+
+            for (Object listElem : src) {
+                TransformResult tr = transform(listElem, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Set) {
+            Set<?> src = (Set<?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Object el : src) {
+                TransformResult tr = transform(el, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Map) {
+            Map<?, ?> src = (Map<?, ?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Map.Entry<?, ?> en : src.entrySet()) {
+                TransformResult k = transform(en.getKey(), depth + 1);
+                TransformResult val = transform(en.getValue(), depth + 1);
+
+                out.put(k.val, val.val);
+                anyChanged |= k.changed || val.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Object[] arrays (non-primitive)
+        if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+            Object[] arr = (Object[])v;
+            Object[] out = new Object[arr.length];
+
+            boolean anyChanged = false;
+
+            for (int i = 0; i < arr.length; i++) {
+                TransformResult tr = transform(arr[i], depth + 1);
+
+                out[i] = tr.val;
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Primitives, String, int[] etc.: unchanged
+        return TransformResult.same(v);
+    }
+
+    /**
+     * Extracts int[] from legacy GridIntList BinaryObject.
+     */
+    private int[] extractInts(BinaryObject oldKeys) {
+        try {
+            Object obj = oldKeys.deserialize();
+
+            if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                int[] res = new int[g.size()];
+
+                for (int i = 0; i < res.length; i++)
+                    res[i] = g.get(i);
+
+                return res;
+            }
+        }
+        catch (Throwable t) {
+            if (verbose)
+                log.info("Deserialize fallback: {}", t.toString());
+        }
+
+        // Hardcode ("arr") based on GridIntList fields
+        Collection<String> childFields = oldKeys.type().fieldNames();
+        if (childFields.contains("arr")) {
+            int[] arr = oldKeys.field("arr");
+
+            if (arr != null)
+                return arr;
+        }
+
+        log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+        return new int[0]; // best effort fallback
+    }
+
+    /**
+     * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+     * or falls back to int[] if the class is not on the classpath.
+     */
+    private Object buildNewKeys(int[] ints) {
+        if (newKeysCachedConstruct != null) {

Review Comment:
   it never null



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2301635959


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());

Review Comment:
   Seems good to scan all caches to prevent cache data loss. There's CLI command --cache to specify caches' names to scan.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315410785


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {

Review Comment:
   Most of migrations are one-shot tasks, see DbMigrations#applyMigration - it skips a migration if it had been already applied. Is there other reason to keep this migration in a separate module?



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "dspavlov (via GitHub)" <gi...@apache.org>.
dspavlov closed pull request #200: IGNITE-21899 added GridInt utils
URL: https://github.com/apache/ignite-teamcity-bot/pull/200


-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2322257199


##########
migrator/src/main/java/migrate/Transformer.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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 src.main.java.migrate;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+ * Guarded by a MAX_DEPTH to avoid accidental cycles.
+ */
+public final class Transformer {
+    /** CLI verbose flag. */
+    private final boolean verbose;
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Logger.
+     */
+    private static final Logger log = GridIntListMigrator.GetMigratorLogger();
+
+    /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+    private final Constructor<?> newKeysCachedConstruct;
+
+    Transformer(boolean verb) {
+        verbose = verb;
+        newKeysCachedConstruct = findNewKeysConstruct();
+    }
+
+    /**
+     * Recursively transforms a value.
+     * <p>
+     * Rules:
+     * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+     * - Java object of OLD_KEYS_TYPE -> same replacement.
+     * - BinaryObject (other type) -> rebuild only if some field changed.
+     * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+     * - Primitives/String/int[] -> unchanged.
+     *
+     * @param v     value to transform
+     * @param depth recursion depth (fixed guard inside)
+     * @return transform result with possibly new value and 'changed' flag
+     */
+    TransformResult transform(Object v, int depth) {
+        if (v == null)
+            return TransformResult.same(null);
+
+        // Strict migration interruption
+        if (depth > MAX_DEPTH) {
+            String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+            throw new IllegalStateException(errMsg);
+        }
+
+        // Binary legacy type
+        if (v instanceof BinaryObject) {
+            BinaryObject bo = (BinaryObject)v;
+            String typeName = bo.type().typeName();
+
+            if (typeName.equals(OLD_KEYS_TYPE)) {
+                int[] ints = extractInts(bo);
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Generic BinaryObject
+            BinaryObjectBuilder bb = bo.toBuilder();
+            boolean anyChanged = false;
+
+            for (String oldChildField : bo.type().fieldNames()) {
+                // Transform nested fields
+                TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                if (childRes.changed) {
+                    bb.setField(oldChildField, childRes.val);
+
+                    anyChanged = true;
+                }
+            }
+
+            // Rebuild only if some field changed
+            return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+        }
+
+        // Java legacy type
+        if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+            org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+            int[] ints = new int[g.size()];
+
+            for (int i = 0; i < ints.length; i++)
+                ints[i] = g.get(i);
+
+            Object newKeys = buildNewKeys(ints);
+
+            return TransformResult.changed(newKeys);
+        }
+
+        // Already new type
+        if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+            return TransformResult.same(v);
+
+        // Collections
+        if (v instanceof List) {
+            List<?> src = (List<?>)v;
+            boolean anyChanged = false;
+
+            List<Object> out = new ArrayList<>(src.size());
+
+            for (Object listElem : src) {
+                TransformResult tr = transform(listElem, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Set) {
+            Set<?> src = (Set<?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Object el : src) {
+                TransformResult tr = transform(el, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Map) {
+            Map<?, ?> src = (Map<?, ?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Map.Entry<?, ?> en : src.entrySet()) {
+                TransformResult k = transform(en.getKey(), depth + 1);
+                TransformResult val = transform(en.getValue(), depth + 1);
+
+                out.put(k.val, val.val);
+                anyChanged |= k.changed || val.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Object[] arrays (non-primitive)
+        if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+            Object[] arr = (Object[])v;
+            Object[] out = new Object[arr.length];
+
+            boolean anyChanged = false;
+
+            for (int i = 0; i < arr.length; i++) {
+                TransformResult tr = transform(arr[i], depth + 1);
+
+                out[i] = tr.val;
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Primitives, String, int[] etc.: unchanged
+        return TransformResult.same(v);
+    }
+
+    /**
+     * Extracts int[] from legacy GridIntList BinaryObject.
+     */
+    private int[] extractInts(BinaryObject oldKeys) {
+        try {
+            Object obj = oldKeys.deserialize();
+
+            if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                int[] res = new int[g.size()];
+
+                for (int i = 0; i < res.length; i++)
+                    res[i] = g.get(i);
+
+                return res;
+            }
+        }
+        catch (Throwable t) {
+            if (verbose)
+                log.info("Deserialize fallback: {}", t.toString());
+        }
+
+        // Hardcode ("arr") based on GridIntList fields
+        Collection<String> childFields = oldKeys.type().fieldNames();
+        if (childFields.contains("arr")) {
+            int[] arr = oldKeys.field("arr");
+
+            if (arr != null)
+                return arr;
+        }
+
+        log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+        return new int[0]; // best effort fallback
+    }
+
+    /**
+     * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+     * or falls back to int[] if the class is not on the classpath.
+     */
+    private Object buildNewKeys(int[] ints) {
+        if (newKeysCachedConstruct != null) {

Review Comment:
   TO-DO: to reflect about ability to tune migrator with third-party constructor



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2301635959


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());

Review Comment:
   Seems good to scan all caches to prevent cache data loss. There's CLI command --cache to specify caches' names to scan/apply if needed.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2303604689


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {

Review Comment:
   This is an online migration class that is called every time the bot is started. It doesnt seem necessary to add a one-time offline migration functionality there.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2315474726


##########
migrator/src/main/java/migrate/Transformer.java:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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 src.main.java.migrate;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.slf4j.Logger;
+
+/**
+ * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+ * Guarded by a MAX_DEPTH to avoid accidental cycles.
+ */
+public final class Transformer {
+    /** CLI verbose flag. */
+    private final boolean verbose;
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Logger.
+     */
+    private static final Logger log = GridIntListMigrator.GetMigratorLogger();
+
+    /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+    private final Constructor<?> newKeysCachedConstruct;
+
+    Transformer(boolean verb) {
+        verbose = verb;
+        newKeysCachedConstruct = findNewKeysConstruct();
+    }
+
+    /**
+     * Recursively transforms a value.
+     * <p>
+     * Rules:
+     * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+     * - Java object of OLD_KEYS_TYPE -> same replacement.
+     * - BinaryObject (other type) -> rebuild only if some field changed.
+     * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+     * - Primitives/String/int[] -> unchanged.
+     *
+     * @param v     value to transform
+     * @param depth recursion depth (fixed guard inside)
+     * @return transform result with possibly new value and 'changed' flag
+     */
+    TransformResult transform(Object v, int depth) {
+        if (v == null)
+            return TransformResult.same(null);
+
+        // Strict migration interruption
+        if (depth > MAX_DEPTH) {
+            String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+            throw new IllegalStateException(errMsg);
+        }
+
+        // Binary legacy type
+        if (v instanceof BinaryObject) {
+            BinaryObject bo = (BinaryObject)v;
+            String typeName = bo.type().typeName();
+
+            if (typeName.equals(OLD_KEYS_TYPE)) {
+                int[] ints = extractInts(bo);
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Generic BinaryObject
+            BinaryObjectBuilder bb = bo.toBuilder();
+            boolean anyChanged = false;
+
+            for (String oldChildField : bo.type().fieldNames()) {
+                // Transform nested fields
+                TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                if (childRes.changed) {
+                    bb.setField(oldChildField, childRes.val);
+
+                    anyChanged = true;
+                }
+            }
+
+            // Rebuild only if some field changed
+            return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+        }
+
+        // Java legacy type
+        if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+            org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+            int[] ints = new int[g.size()];
+
+            for (int i = 0; i < ints.length; i++)
+                ints[i] = g.get(i);
+
+            Object newKeys = buildNewKeys(ints);
+
+            return TransformResult.changed(newKeys);
+        }
+
+        // Already new type
+        if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+            return TransformResult.same(v);
+
+        // Collections
+        if (v instanceof List) {
+            List<?> src = (List<?>)v;
+            boolean anyChanged = false;
+
+            List<Object> out = new ArrayList<>(src.size());
+
+            for (Object listElem : src) {
+                TransformResult tr = transform(listElem, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Set) {
+            Set<?> src = (Set<?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Object el : src) {
+                TransformResult tr = transform(el, depth + 1);
+
+                out.add(tr.val);
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        if (v instanceof Map) {
+            Map<?, ?> src = (Map<?, ?>)v;
+            boolean anyChanged = false;
+
+            LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+            for (Map.Entry<?, ?> en : src.entrySet()) {
+                TransformResult k = transform(en.getKey(), depth + 1);
+                TransformResult val = transform(en.getValue(), depth + 1);
+
+                out.put(k.val, val.val);
+                anyChanged |= k.changed || val.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Object[] arrays (non-primitive)
+        if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+            Object[] arr = (Object[])v;
+            Object[] out = new Object[arr.length];
+
+            boolean anyChanged = false;
+
+            for (int i = 0; i < arr.length; i++) {
+                TransformResult tr = transform(arr[i], depth + 1);
+
+                out[i] = tr.val;
+                anyChanged |= tr.changed;
+            }
+
+            return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+        }
+
+        // Primitives, String, int[] etc.: unchanged
+        return TransformResult.same(v);
+    }
+
+    /**
+     * Extracts int[] from legacy GridIntList BinaryObject.
+     */
+    private int[] extractInts(BinaryObject oldKeys) {
+        try {
+            Object obj = oldKeys.deserialize();
+
+            if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                int[] res = new int[g.size()];
+
+                for (int i = 0; i < res.length; i++)
+                    res[i] = g.get(i);

Review Comment:
   Can we use System.arrayCopy instead?



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "FewJuho (via GitHub)" <gi...@apache.org>.
FewJuho commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2301620303


##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());
+
+            if (a.cacheFilter != null && !a.cacheFilter.isEmpty())
+                cacheNames.removeIf(n -> !n.contains(a.cacheFilter));
+
+            log.info("Caches to scan: {}", cacheNames);
+
+            Transformer transformer = new Transformer(a.verbose);
+
+            for (String cacheName : cacheNames) {
+                IgniteCache<Object, Object> rawCache = ig.cache(cacheName);
+
+                if (rawCache == null)
+                    continue;
+
+                IgniteCache<Object, Object> c = rawCache.withKeepBinary();
+
+                log.info("Scanning cache: {}", cacheName);
+
+                ScanQuery<Object, Object> q = new ScanQuery<>();
+                q.setPageSize(DEFAULT_PAGE_SIZE);
+
+                AtomicLong scanned = new AtomicLong();
+                AtomicLong updated = new AtomicLong();
+
+                try (QueryCursor<Cache.Entry<Object, Object>> cur = c.query(q)) {
+                    for (Cache.Entry<Object, Object> e : cur) {
+                        try {
+                            Object v = e.getValue();
+                            TransformResult tr = transformer.transform(v, 0);
+
+                            if (tr.changed) {
+                                if (a.apply) {
+                                    c.put(e.getKey(), tr.val);
+
+                                    updated.incrementAndGet();
+                                }
+                                else if (a.verbose)
+                                    log.info("DRY-RUN would update key={}", e.getKey());
+                            }
+
+                            long s = scanned.incrementAndGet();
+
+                            if (s % a.reportEvery == 0)
+                                log.info("Scanned={} updated={}", s, updated.get());
+                        }
+                        catch (Throwable t) {
+                            log.warn("Entry migration failed, skipping. Cause: {}", t.toString());
+
+                            scanned.incrementAndGet();
+                        }
+                    }
+                }
+
+                log.info("Done {}: scanned={} updated={}", cacheName, scanned.get(), updated.get());
+            }
+
+            log.info("Migration finished.");
+        }
+    }
+
+    /**
+     * Infers consistentId by reading first subdirectory under work/db.
+     *
+     * @param workDir path to Ignite work dir.
+     * @return consistentId or null if not found.
+     */
+    private static String detectConsistentId(String workDir) {
+        File dbDir = new File(workDir, "db");
+
+        if (!dbDir.isDirectory())
+            return null;
+
+        File[] kids = dbDir.listFiles(File::isDirectory);
+
+        if (kids == null || kids.length == 0)
+            return null;
+
+        return kids[0].getName();
+    }
+
+    /**
+     * Result of a recursive transformation: value and flag indicating changes.
+     */
+    private static final class TransformResult {
+        /** Value. */
+        final Object val;
+
+        /** Changed. */
+        final boolean changed;
+
+        private TransformResult(Object val, boolean changed) {
+            this.val = val;
+            this.changed = changed;
+        }
+
+        static TransformResult same(Object v) {
+            return new TransformResult(v, false);
+        }
+
+        static TransformResult changed(Object v) {
+            return new TransformResult(v, true);
+        }
+    }
+
+    /**
+     * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+     * Guarded by a MAX_DEPTH to avoid accidental cycles.
+     */
+    private static final class Transformer {
+        private final boolean verbose;
+
+        /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+        private final Constructor<?> newKeysCachedConstruct;
+
+        Transformer(boolean verb) {
+            verbose = verb;
+            newKeysCachedConstruct = findNewKeysConstruct();
+        }
+
+        /**
+         * Recursively transforms a value.
+         * <p>
+         * Rules:
+         * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+         * - Java object of OLD_KEYS_TYPE -> same replacement.
+         * - BinaryObject (other type) -> rebuild only if some field changed.
+         * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+         * - Primitives/String/int[] -> unchanged.
+         *
+         * @param v     value to transform
+         * @param depth recursion depth (fixed guard inside)
+         * @return transform result with possibly new value and 'changed' flag
+         */
+        TransformResult transform(Object v, int depth) {
+            if (v == null)
+                return TransformResult.same(null);
+
+            // Strict migration interruption
+            if (depth > MAX_DEPTH) {
+                String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+                throw new IllegalStateException(errMsg);
+            }
+
+            // Binary legacy type
+            if (v instanceof BinaryObject) {
+                BinaryObject bo = (BinaryObject)v;
+                String typeName = bo.type().typeName();
+
+                if (typeName.equals(OLD_KEYS_TYPE)) {
+                    int[] ints = extractInts(bo);
+                    Object newKeys = buildNewKeys(ints);
+
+                    return TransformResult.changed(newKeys);
+                }
+
+                // Generic BinaryObject
+                BinaryObjectBuilder bb = bo.toBuilder();
+                boolean anyChanged = false;
+
+                for (String oldChildField : bo.type().fieldNames()) {
+                    // Transform nested fields
+                    TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                    if (childRes.changed) {
+                        bb.setField(oldChildField, childRes.val);
+
+                        anyChanged = true;
+                    }
+                }
+
+                // Rebuild only if some field changed
+                return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+            }
+
+            // Java legacy type
+            if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+                int[] ints = new int[g.size()];
+
+                for (int i = 0; i < ints.length; i++)
+                    ints[i] = g.get(i);
+
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Already new type
+            if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+                return TransformResult.same(v);
+
+            // Collections
+            if (v instanceof List) {
+                List<?> src = (List<?>)v;
+                boolean anyChanged = false;
+
+                List<Object> out = new ArrayList<>(src.size());
+
+                for (Object listElem : src) {
+                    TransformResult tr = transform(listElem, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Set) {
+                Set<?> src = (Set<?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Object el : src) {
+                    TransformResult tr = transform(el, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Map) {
+                Map<?, ?> src = (Map<?, ?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Map.Entry<?, ?> en : src.entrySet()) {
+                    TransformResult k = transform(en.getKey(), depth + 1);
+                    TransformResult val = transform(en.getValue(), depth + 1);
+
+                    out.put(k.val, val.val);
+                    anyChanged |= k.changed || val.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Object[] arrays (non-primitive)
+            if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+                Object[] arr = (Object[])v;
+                Object[] out = new Object[arr.length];
+
+                boolean anyChanged = false;
+
+                for (int i = 0; i < arr.length; i++) {
+                    TransformResult tr = transform(arr[i], depth + 1);
+
+                    out[i] = tr.val;
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Primitives, String, int[] etc.: unchanged
+            return TransformResult.same(v);
+        }
+
+        /**
+         * Extracts int[] from legacy GridIntList BinaryObject.
+         */
+        private int[] extractInts(BinaryObject oldKeys) {
+            try {
+                Object obj = oldKeys.deserialize();
+
+                if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                    org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                    int[] res = new int[g.size()];
+
+                    for (int i = 0; i < res.length; i++)
+                        res[i] = g.get(i);
+
+                    return res;
+                }
+            }
+            catch (Throwable t) {
+                if (verbose)
+                    log.info("Deserialize fallback: {}", t.toString());
+            }
+
+            // Hardcode ("arr") based on GridIntList fields
+            Collection<String> childFields = oldKeys.type().fieldNames();
+            if (childFields.contains("arr")) {
+                int[] arr = oldKeys.field("arr");
+
+                if (arr != null)
+                    return arr;
+            }
+
+            log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+            return new int[0]; // best effort fallback
+        }
+
+        /**
+         * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+         * or falls back to int[] if the class is not on the classpath.
+         */
+        private Object buildNewKeys(int[] ints) {
+            if (newKeysCachedConstruct != null) {
+                try {
+                    return newKeysCachedConstruct.newInstance((Object)ints);
+                }
+                catch (ReflectiveOperationException ignored) {
+                    // fall through to fallback
+                }
+            }
+
+            if (verbose)
+                log.warn("NEW_KEYS_TYPE {} is not available; falling back to raw int[]", NEW_KEYS_TYPE);
+
+            return ints; // best effort fallback
+        }
+
+        /**
+         * Resolves constructor NEW_KEYS_TYPE(int[]) once to avoid reflection per entry.
+         */
+        private Constructor<?> findNewKeysConstruct() {
+            try {
+                Class<?> cls = Class.forName(NEW_KEYS_TYPE);
+
+                return cls.getConstructor(int[].class);
+            }
+            catch (Throwable t) {
+                if (verbose) {
+                    log.warn("New keys type {} is not on classpath, will fallback to int[] (cause: {})",

Review Comment:
   Is it better to fail an entire build? Looks like in that case we can load third party constructor to proceed migration.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "timoninmaxim (via GitHub)" <gi...@apache.org>.
timoninmaxim commented on code in PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#discussion_r2300808841


##########
build.gradle:
##########
@@ -46,7 +46,7 @@ allprojects {
 
         jettyVer = '9.4.12.v20180830'
 
-        ignVer = '2.14.0'
+        ignVer = '2.16.0'

Review Comment:
   2.17.0 is actual version



##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());

Review Comment:
   Do we need scan all caches? 



##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());
+
+            if (a.cacheFilter != null && !a.cacheFilter.isEmpty())
+                cacheNames.removeIf(n -> !n.contains(a.cacheFilter));
+
+            log.info("Caches to scan: {}", cacheNames);
+
+            Transformer transformer = new Transformer(a.verbose);
+
+            for (String cacheName : cacheNames) {
+                IgniteCache<Object, Object> rawCache = ig.cache(cacheName);
+
+                if (rawCache == null)
+                    continue;
+
+                IgniteCache<Object, Object> c = rawCache.withKeepBinary();
+
+                log.info("Scanning cache: {}", cacheName);
+
+                ScanQuery<Object, Object> q = new ScanQuery<>();
+                q.setPageSize(DEFAULT_PAGE_SIZE);
+
+                AtomicLong scanned = new AtomicLong();
+                AtomicLong updated = new AtomicLong();
+
+                try (QueryCursor<Cache.Entry<Object, Object>> cur = c.query(q)) {
+                    for (Cache.Entry<Object, Object> e : cur) {
+                        try {
+                            Object v = e.getValue();
+                            TransformResult tr = transformer.transform(v, 0);
+
+                            if (tr.changed) {
+                                if (a.apply) {
+                                    c.put(e.getKey(), tr.val);
+
+                                    updated.incrementAndGet();
+                                }
+                                else if (a.verbose)
+                                    log.info("DRY-RUN would update key={}", e.getKey());
+                            }
+
+                            long s = scanned.incrementAndGet();
+
+                            if (s % a.reportEvery == 0)
+                                log.info("Scanned={} updated={}", s, updated.get());
+                        }
+                        catch (Throwable t) {
+                            log.warn("Entry migration failed, skipping. Cause: {}", t.toString());
+
+                            scanned.incrementAndGet();
+                        }
+                    }
+                }
+
+                log.info("Done {}: scanned={} updated={}", cacheName, scanned.get(), updated.get());
+            }
+
+            log.info("Migration finished.");
+        }
+    }
+
+    /**
+     * Infers consistentId by reading first subdirectory under work/db.
+     *
+     * @param workDir path to Ignite work dir.
+     * @return consistentId or null if not found.
+     */
+    private static String detectConsistentId(String workDir) {
+        File dbDir = new File(workDir, "db");
+
+        if (!dbDir.isDirectory())
+            return null;
+
+        File[] kids = dbDir.listFiles(File::isDirectory);
+
+        if (kids == null || kids.length == 0)
+            return null;
+
+        return kids[0].getName();
+    }
+
+    /**
+     * Result of a recursive transformation: value and flag indicating changes.
+     */
+    private static final class TransformResult {
+        /** Value. */
+        final Object val;
+
+        /** Changed. */
+        final boolean changed;
+
+        private TransformResult(Object val, boolean changed) {
+            this.val = val;
+            this.changed = changed;
+        }
+
+        static TransformResult same(Object v) {
+            return new TransformResult(v, false);
+        }
+
+        static TransformResult changed(Object v) {
+            return new TransformResult(v, true);
+        }
+    }
+
+    /**
+     * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+     * Guarded by a MAX_DEPTH to avoid accidental cycles.
+     */
+    private static final class Transformer {
+        private final boolean verbose;
+
+        /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+        private final Constructor<?> newKeysCachedConstruct;
+
+        Transformer(boolean verb) {
+            verbose = verb;
+            newKeysCachedConstruct = findNewKeysConstruct();
+        }
+
+        /**
+         * Recursively transforms a value.
+         * <p>
+         * Rules:
+         * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+         * - Java object of OLD_KEYS_TYPE -> same replacement.
+         * - BinaryObject (other type) -> rebuild only if some field changed.
+         * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+         * - Primitives/String/int[] -> unchanged.
+         *
+         * @param v     value to transform
+         * @param depth recursion depth (fixed guard inside)
+         * @return transform result with possibly new value and 'changed' flag
+         */
+        TransformResult transform(Object v, int depth) {
+            if (v == null)
+                return TransformResult.same(null);
+
+            // Strict migration interruption
+            if (depth > MAX_DEPTH) {
+                String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+                throw new IllegalStateException(errMsg);
+            }
+
+            // Binary legacy type
+            if (v instanceof BinaryObject) {
+                BinaryObject bo = (BinaryObject)v;
+                String typeName = bo.type().typeName();
+
+                if (typeName.equals(OLD_KEYS_TYPE)) {
+                    int[] ints = extractInts(bo);
+                    Object newKeys = buildNewKeys(ints);
+
+                    return TransformResult.changed(newKeys);
+                }
+
+                // Generic BinaryObject
+                BinaryObjectBuilder bb = bo.toBuilder();
+                boolean anyChanged = false;
+
+                for (String oldChildField : bo.type().fieldNames()) {
+                    // Transform nested fields
+                    TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                    if (childRes.changed) {
+                        bb.setField(oldChildField, childRes.val);
+
+                        anyChanged = true;
+                    }
+                }
+
+                // Rebuild only if some field changed
+                return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+            }
+
+            // Java legacy type
+            if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+                int[] ints = new int[g.size()];
+
+                for (int i = 0; i < ints.length; i++)
+                    ints[i] = g.get(i);
+
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Already new type
+            if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+                return TransformResult.same(v);
+
+            // Collections
+            if (v instanceof List) {
+                List<?> src = (List<?>)v;
+                boolean anyChanged = false;
+
+                List<Object> out = new ArrayList<>(src.size());
+
+                for (Object listElem : src) {
+                    TransformResult tr = transform(listElem, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Set) {
+                Set<?> src = (Set<?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Object el : src) {
+                    TransformResult tr = transform(el, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Map) {
+                Map<?, ?> src = (Map<?, ?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Map.Entry<?, ?> en : src.entrySet()) {
+                    TransformResult k = transform(en.getKey(), depth + 1);
+                    TransformResult val = transform(en.getValue(), depth + 1);
+
+                    out.put(k.val, val.val);
+                    anyChanged |= k.changed || val.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Object[] arrays (non-primitive)
+            if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+                Object[] arr = (Object[])v;
+                Object[] out = new Object[arr.length];
+
+                boolean anyChanged = false;
+
+                for (int i = 0; i < arr.length; i++) {
+                    TransformResult tr = transform(arr[i], depth + 1);
+
+                    out[i] = tr.val;
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Primitives, String, int[] etc.: unchanged
+            return TransformResult.same(v);
+        }
+
+        /**
+         * Extracts int[] from legacy GridIntList BinaryObject.
+         */
+        private int[] extractInts(BinaryObject oldKeys) {
+            try {
+                Object obj = oldKeys.deserialize();
+
+                if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                    org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                    int[] res = new int[g.size()];
+
+                    for (int i = 0; i < res.length; i++)
+                        res[i] = g.get(i);
+
+                    return res;
+                }
+            }
+            catch (Throwable t) {
+                if (verbose)
+                    log.info("Deserialize fallback: {}", t.toString());
+            }
+
+            // Hardcode ("arr") based on GridIntList fields
+            Collection<String> childFields = oldKeys.type().fieldNames();
+            if (childFields.contains("arr")) {
+                int[] arr = oldKeys.field("arr");
+
+                if (arr != null)
+                    return arr;
+            }
+
+            log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+            return new int[0]; // best effort fallback
+        }
+
+        /**
+         * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+         * or falls back to int[] if the class is not on the classpath.
+         */
+        private Object buildNewKeys(int[] ints) {
+            if (newKeysCachedConstruct != null) {
+                try {
+                    return newKeysCachedConstruct.newInstance((Object)ints);
+                }
+                catch (ReflectiveOperationException ignored) {
+                    // fall through to fallback
+                }
+            }
+
+            if (verbose)
+                log.warn("NEW_KEYS_TYPE {} is not available; falling back to raw int[]", NEW_KEYS_TYPE);
+
+            return ints; // best effort fallback
+        }
+
+        /**
+         * Resolves constructor NEW_KEYS_TYPE(int[]) once to avoid reflection per entry.
+         */
+        private Constructor<?> findNewKeysConstruct() {
+            try {
+                Class<?> cls = Class.forName(NEW_KEYS_TYPE);
+
+                return cls.getConstructor(int[].class);
+            }
+            catch (Throwable t) {
+                if (verbose) {
+                    log.warn("New keys type {} is not on classpath, will fallback to int[] (cause: {})",

Review Comment:
   At which circumstance we should proceed if class not found?



##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {

Review Comment:
   I see we have DbMigrations class with caches/data migrations. Can we built this code in the class?



##########
migrator/src/main/java/migrate/GridIntListMigrator.java:
##########
@@ -0,0 +1,532 @@
+/*
+ * 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 migrate;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.ScanQuery;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.cache.Cache;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Offline migrator for TeamCity Bot Ignite persistence.
+ * <p>
+ * Recursively scans all entries in Ignite caches and replaces any occurrence of the legacy type
+ * org.apache.ignite.internal.util.GridIntList with the new type
+ * org.apache.ignite.tcbot.common.util.GridIntList, preserving the int[] payload.
+ * <p>
+ * Usage:
+ * export IGNITE_WORK_DIR=/abs/path/to/work_backup
+ * ./gradlew -p migrator run --args="--verbose --report 200"      # dry run with verbose report
+ * ./gradlew -p migrator run --args="--apply --report 500"        # apply to all caches
+ */
+
+public final class GridIntListMigrator {
+    /**
+     * Logger.
+     */
+    private static final Logger log = LoggerFactory.getLogger(GridIntListMigrator.class);
+
+    /**
+     * Fully-qualified name of the legacy type to replace.
+     */
+    private static final String OLD_KEYS_TYPE = "org.apache.ignite.internal.util.GridIntList";
+
+    /**
+     * Fully-qualified name of the new type.
+     */
+    private static final String NEW_KEYS_TYPE = "org.apache.ignite.tcbot.common.util.GridIntList";
+
+    /**
+     * Scan page size.
+     */
+    private static final int DEFAULT_PAGE_SIZE = 256;
+
+    /**
+     * Recursion guard to prevent accidental cycles.
+     */
+    private static final int MAX_DEPTH = 32;
+
+    /**
+     * Default constructor.
+     */
+    private GridIntListMigrator() {
+    }
+
+    /**
+     * Boots a standalone Ignite node on the given work dir and executes migration.
+     */
+    public static void main(String[] args) {
+        System.setProperty("java.net.preferIPv4Stack", "true");
+
+        Args a = Args.parse(args);
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            String wd = System.getProperty("IGNITE_WORK_DIR");
+
+            if (wd == null || wd.isEmpty())
+                wd = System.getenv("IGNITE_WORK_DIR");
+
+            a.workDir = wd;
+        }
+
+        if (a.workDir == null || a.workDir.isEmpty()) {
+            log.error("IGNITE_WORK_DIR is not set. Use --workDir or set system/env variable.");
+
+            System.exit(2);
+        }
+
+        File checkDir = new File(a.workDir);
+
+        if (!checkDir.isDirectory()) {
+            log.error("IGNITE_WORK_DIR is not a directory: {}", checkDir.getAbsolutePath());
+
+            System.exit(2);
+        }
+
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setIgniteInstanceName("tcbot-migrator")
+            .setWorkDirectory(a.workDir);
+
+        String consistentId = detectConsistentId(a.workDir);
+
+        if (consistentId != null) {
+            cfg.setConsistentId(consistentId);
+
+            log.info("Using consistentId={}", consistentId);
+        }
+        else
+            log.warn("Couldnt detect consistentId.");
+
+        DataStorageConfiguration ds = new DataStorageConfiguration();
+        ds.getDefaultDataRegionConfiguration().setPersistenceEnabled(true);
+        cfg.setDataStorageConfiguration(ds);
+
+        BinaryConfiguration bcfg = new BinaryConfiguration();
+        cfg.setBinaryConfiguration(bcfg);
+
+        Ignition.setClientMode(false);
+
+        try (Ignite ig = Ignition.start(cfg)) {
+            ig.cluster().state(ClusterState.ACTIVE);
+
+            Collection<String> cacheNames = new ArrayList<>(ig.cacheNames());
+
+            if (a.cacheFilter != null && !a.cacheFilter.isEmpty())
+                cacheNames.removeIf(n -> !n.contains(a.cacheFilter));
+
+            log.info("Caches to scan: {}", cacheNames);
+
+            Transformer transformer = new Transformer(a.verbose);
+
+            for (String cacheName : cacheNames) {
+                IgniteCache<Object, Object> rawCache = ig.cache(cacheName);
+
+                if (rawCache == null)
+                    continue;
+
+                IgniteCache<Object, Object> c = rawCache.withKeepBinary();
+
+                log.info("Scanning cache: {}", cacheName);
+
+                ScanQuery<Object, Object> q = new ScanQuery<>();
+                q.setPageSize(DEFAULT_PAGE_SIZE);
+
+                AtomicLong scanned = new AtomicLong();
+                AtomicLong updated = new AtomicLong();
+
+                try (QueryCursor<Cache.Entry<Object, Object>> cur = c.query(q)) {
+                    for (Cache.Entry<Object, Object> e : cur) {
+                        try {
+                            Object v = e.getValue();
+                            TransformResult tr = transformer.transform(v, 0);
+
+                            if (tr.changed) {
+                                if (a.apply) {
+                                    c.put(e.getKey(), tr.val);
+
+                                    updated.incrementAndGet();
+                                }
+                                else if (a.verbose)
+                                    log.info("DRY-RUN would update key={}", e.getKey());
+                            }
+
+                            long s = scanned.incrementAndGet();
+
+                            if (s % a.reportEvery == 0)
+                                log.info("Scanned={} updated={}", s, updated.get());
+                        }
+                        catch (Throwable t) {
+                            log.warn("Entry migration failed, skipping. Cause: {}", t.toString());
+
+                            scanned.incrementAndGet();
+                        }
+                    }
+                }
+
+                log.info("Done {}: scanned={} updated={}", cacheName, scanned.get(), updated.get());
+            }
+
+            log.info("Migration finished.");
+        }
+    }
+
+    /**
+     * Infers consistentId by reading first subdirectory under work/db.
+     *
+     * @param workDir path to Ignite work dir.
+     * @return consistentId or null if not found.
+     */
+    private static String detectConsistentId(String workDir) {
+        File dbDir = new File(workDir, "db");
+
+        if (!dbDir.isDirectory())
+            return null;
+
+        File[] kids = dbDir.listFiles(File::isDirectory);
+
+        if (kids == null || kids.length == 0)
+            return null;
+
+        return kids[0].getName();
+    }
+
+    /**
+     * Result of a recursive transformation: value and flag indicating changes.
+     */
+    private static final class TransformResult {
+        /** Value. */
+        final Object val;
+
+        /** Changed. */
+        final boolean changed;
+
+        private TransformResult(Object val, boolean changed) {
+            this.val = val;
+            this.changed = changed;
+        }
+
+        static TransformResult same(Object v) {
+            return new TransformResult(v, false);
+        }
+
+        static TransformResult changed(Object v) {
+            return new TransformResult(v, true);
+        }
+    }
+
+    /**
+     * Performs recursive transformation of values, replacing legacy GridIntList with the new type.
+     * Guarded by a MAX_DEPTH to avoid accidental cycles.
+     */
+    private static final class Transformer {
+        private final boolean verbose;
+
+        /** Cached constructor of NEW_KEYS_TYPE(int[]) to avoid excessive reflection. */
+        private final Constructor<?> newKeysCachedConstruct;
+
+        Transformer(boolean verb) {
+            verbose = verb;
+            newKeysCachedConstruct = findNewKeysConstruct();
+        }
+
+        /**
+         * Recursively transforms a value.
+         * <p>
+         * Rules:
+         * - BinaryObject of OLD_KEYS_TYPE -> build NEW_KEYS_TYPE or fallback to int[].
+         * - Java object of OLD_KEYS_TYPE -> same replacement.
+         * - BinaryObject (other type) -> rebuild only if some field changed.
+         * - List/Set/Map/Object[] -> rebuild container only if any element changed.
+         * - Primitives/String/int[] -> unchanged.
+         *
+         * @param v     value to transform
+         * @param depth recursion depth (fixed guard inside)
+         * @return transform result with possibly new value and 'changed' flag
+         */
+        TransformResult transform(Object v, int depth) {
+            if (v == null)
+                return TransformResult.same(null);
+
+            // Strict migration interruption
+            if (depth > MAX_DEPTH) {
+                String errMsg = String.format("Max depth %d reached; value type=%s, aborting migration.", MAX_DEPTH, v.getClass());
+
+                throw new IllegalStateException(errMsg);
+            }
+
+            // Binary legacy type
+            if (v instanceof BinaryObject) {
+                BinaryObject bo = (BinaryObject)v;
+                String typeName = bo.type().typeName();
+
+                if (typeName.equals(OLD_KEYS_TYPE)) {
+                    int[] ints = extractInts(bo);
+                    Object newKeys = buildNewKeys(ints);
+
+                    return TransformResult.changed(newKeys);
+                }
+
+                // Generic BinaryObject
+                BinaryObjectBuilder bb = bo.toBuilder();
+                boolean anyChanged = false;
+
+                for (String oldChildField : bo.type().fieldNames()) {
+                    // Transform nested fields
+                    TransformResult childRes = transform(bo.field(oldChildField), depth + 1);
+
+                    if (childRes.changed) {
+                        bb.setField(oldChildField, childRes.val);
+
+                        anyChanged = true;
+                    }
+                }
+
+                // Rebuild only if some field changed
+                return anyChanged ? TransformResult.changed(bb.build()) : TransformResult.same(v);
+            }
+
+            // Java legacy type
+            if (v instanceof org.apache.ignite.internal.util.GridIntList) {
+                org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)v;
+                int[] ints = new int[g.size()];
+
+                for (int i = 0; i < ints.length; i++)
+                    ints[i] = g.get(i);
+
+                Object newKeys = buildNewKeys(ints);
+
+                return TransformResult.changed(newKeys);
+            }
+
+            // Already new type
+            if (v.getClass().getName().equals(NEW_KEYS_TYPE))
+                return TransformResult.same(v);
+
+            // Collections
+            if (v instanceof List) {
+                List<?> src = (List<?>)v;
+                boolean anyChanged = false;
+
+                List<Object> out = new ArrayList<>(src.size());
+
+                for (Object listElem : src) {
+                    TransformResult tr = transform(listElem, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Set) {
+                Set<?> src = (Set<?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashSet<Object> out = new LinkedHashSet<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Object el : src) {
+                    TransformResult tr = transform(el, depth + 1);
+
+                    out.add(tr.val);
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            if (v instanceof Map) {
+                Map<?, ?> src = (Map<?, ?>)v;
+                boolean anyChanged = false;
+
+                LinkedHashMap<Object, Object> out = new LinkedHashMap<>(Math.max(16, (int)Math.ceil(src.size() / 0.75)));
+
+                for (Map.Entry<?, ?> en : src.entrySet()) {
+                    TransformResult k = transform(en.getKey(), depth + 1);
+                    TransformResult val = transform(en.getValue(), depth + 1);
+
+                    out.put(k.val, val.val);
+                    anyChanged |= k.changed || val.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Object[] arrays (non-primitive)
+            if (v.getClass().isArray() && !v.getClass().getComponentType().isPrimitive()) {
+                Object[] arr = (Object[])v;
+                Object[] out = new Object[arr.length];
+
+                boolean anyChanged = false;
+
+                for (int i = 0; i < arr.length; i++) {
+                    TransformResult tr = transform(arr[i], depth + 1);
+
+                    out[i] = tr.val;
+                    anyChanged |= tr.changed;
+                }
+
+                return anyChanged ? TransformResult.changed(out) : TransformResult.same(v);
+            }
+
+            // Primitives, String, int[] etc.: unchanged
+            return TransformResult.same(v);
+        }
+
+        /**
+         * Extracts int[] from legacy GridIntList BinaryObject.
+         */
+        private int[] extractInts(BinaryObject oldKeys) {
+            try {
+                Object obj = oldKeys.deserialize();
+
+                if (obj instanceof org.apache.ignite.internal.util.GridIntList) {
+                    org.apache.ignite.internal.util.GridIntList g = (org.apache.ignite.internal.util.GridIntList)obj;
+
+                    int[] res = new int[g.size()];
+
+                    for (int i = 0; i < res.length; i++)
+                        res[i] = g.get(i);
+
+                    return res;
+                }
+            }
+            catch (Throwable t) {
+                if (verbose)
+                    log.info("Deserialize fallback: {}", t.toString());
+            }
+
+            // Hardcode ("arr") based on GridIntList fields
+            Collection<String> childFields = oldKeys.type().fieldNames();
+            if (childFields.contains("arr")) {
+                int[] arr = oldKeys.field("arr");
+
+                if (arr != null)
+                    return arr;
+            }
+
+            log.warn("Can't extract ints from {} fields={}", oldKeys.type().typeName(), childFields);
+
+            return new int[0]; // best effort fallback
+        }
+
+        /**
+         * Builds an instance of the new GridIntList (org.apache.ignite.tcbot.common.util.GridIntList),
+         * or falls back to int[] if the class is not on the classpath.
+         */
+        private Object buildNewKeys(int[] ints) {
+            if (newKeysCachedConstruct != null) {
+                try {
+                    return newKeysCachedConstruct.newInstance((Object)ints);
+                }
+                catch (ReflectiveOperationException ignored) {
+                    // fall through to fallback
+                }
+            }
+
+            if (verbose)
+                log.warn("NEW_KEYS_TYPE {} is not available; falling back to raw int[]", NEW_KEYS_TYPE);
+
+            return ints; // best effort fallback
+        }
+
+        /**
+         * Resolves constructor NEW_KEYS_TYPE(int[]) once to avoid reflection per entry.
+         */
+        private Constructor<?> findNewKeysConstruct() {
+            try {
+                Class<?> cls = Class.forName(NEW_KEYS_TYPE);
+
+                return cls.getConstructor(int[].class);
+            }
+            catch (Throwable t) {
+                if (verbose) {
+                    log.warn("New keys type {} is not on classpath, will fallback to int[] (cause: {})",
+                        NEW_KEYS_TYPE, t.toString());
+                }
+
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Command-line arguments.
+     * <p>
+     * Supported flags:
+     * --apply           actually write changes (otherwise dry-run)
+     * --verbose         more diagnostics
+     * --cache <substr>  process only caches whose name contains given substring
+     * --report <N>      progress log interval
+     * --workDir <path>  path to work/ directory (overrides IGNITE_WORK_DIR)
+     */
+    static final class Args {

Review Comment:
   Let's create separate files for each package-private class: Args, Transformer, TransformResult.



-- 
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-21899 added GridInt utils [ignite-teamcity-bot]

Posted by "dspavlov (via GitHub)" <gi...@apache.org>.
dspavlov commented on PR #200:
URL: https://github.com/apache/ignite-teamcity-bot/pull/200#issuecomment-4346130238

   resolved by #206 , thank you for your contribution


-- 
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