|
|
dc8c34 |
From 183b91f804e1487f459c4ddf358447b4c01e311c Mon Sep 17 00:00:00 2001
|
|
|
dc8c34 |
From: "Thierry bordaz (tbordaz)" <tbordaz@redhat.com>
|
|
|
dc8c34 |
Date: Wed, 15 Jan 2014 09:59:13 +0100
|
|
|
dc8c34 |
Subject: [PATCH 404/404] Ticket 47619: cannot reindex retrochangelog
|
|
|
dc8c34 |
|
|
|
dc8c34 |
Bug Description:
|
|
|
dc8c34 |
An index task (through db2index.pl) on changelog backend may hang.
|
|
|
dc8c34 |
The reason is that the retrocl-plugin start function (retrocl_start) acquires the
|
|
|
dc8c34 |
changelog backend in read (slapi_be_Rlock) but does not release it.
|
|
|
dc8c34 |
The task will try to acquire it in Write but will wait indefinitely.
|
|
|
dc8c34 |
|
|
|
dc8c34 |
Fix Description:
|
|
|
dc8c34 |
Make retrocl_start to release the lock acquired in Read in slapi_mapping_tree_select
|
|
|
dc8c34 |
|
|
|
dc8c34 |
https://fedorahosted.org/389/ticket/47619
|
|
|
dc8c34 |
|
|
|
dc8c34 |
Reviewed by: Rich Megginson
|
|
|
dc8c34 |
|
|
|
dc8c34 |
Platforms tested: F17
|
|
|
dc8c34 |
|
|
|
dc8c34 |
Flag Day: no
|
|
|
dc8c34 |
|
|
|
dc8c34 |
Doc impact: no
|
|
|
dc8c34 |
|
|
|
dc8c34 |
(cherry picked from commit 8087f058cd9acc304de351412742fd8ceb3ceb48)
|
|
|
dc8c34 |
(cherry picked from commit a5499cbf60c6b8277f0acffed8763ffe52ae4aff)
|
|
|
dc8c34 |
---
|
|
|
dc8c34 |
dirsrvtests/tickets/ticket47619_test.py | 299 ++++++++++++++++++++++++++++++++
|
|
|
dc8c34 |
ldap/servers/plugins/retrocl/retrocl.c | 6 +-
|
|
|
dc8c34 |
2 files changed, 303 insertions(+), 2 deletions(-)
|
|
|
dc8c34 |
create mode 100644 dirsrvtests/tickets/ticket47619_test.py
|
|
|
dc8c34 |
|
|
|
dc8c34 |
diff --git a/dirsrvtests/tickets/ticket47619_test.py b/dirsrvtests/tickets/ticket47619_test.py
|
|
|
dc8c34 |
new file mode 100644
|
|
|
dc8c34 |
index 0000000..c3eae8d
|
|
|
dc8c34 |
--- /dev/null
|
|
|
dc8c34 |
+++ b/dirsrvtests/tickets/ticket47619_test.py
|
|
|
dc8c34 |
@@ -0,0 +1,299 @@
|
|
|
dc8c34 |
+'''
|
|
|
dc8c34 |
+Created on Nov 7, 2013
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+@author: tbordaz
|
|
|
dc8c34 |
+'''
|
|
|
dc8c34 |
+import os
|
|
|
dc8c34 |
+import sys
|
|
|
dc8c34 |
+import time
|
|
|
dc8c34 |
+import ldap
|
|
|
dc8c34 |
+import logging
|
|
|
dc8c34 |
+import socket
|
|
|
dc8c34 |
+import time
|
|
|
dc8c34 |
+import logging
|
|
|
dc8c34 |
+import pytest
|
|
|
dc8c34 |
+import re
|
|
|
dc8c34 |
+from lib389 import DirSrv, Entry, tools
|
|
|
dc8c34 |
+from lib389.tools import DirSrvTools
|
|
|
dc8c34 |
+from lib389._constants import *
|
|
|
dc8c34 |
+from lib389.properties import *
|
|
|
dc8c34 |
+from constants import *
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+logging.getLogger(__name__).setLevel(logging.DEBUG)
|
|
|
dc8c34 |
+log = logging.getLogger(__name__)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+installation_prefix = None
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
|
|
|
dc8c34 |
+ENTRY_DN = "cn=test_entry, %s" % SUFFIX
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+OTHER_NAME = 'other_entry'
|
|
|
dc8c34 |
+MAX_OTHERS = 100
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ATTRIBUTES = [ 'street', 'countryName', 'description', 'postalAddress', 'postalCode', 'title', 'l', 'roomNumber' ]
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+class TopologyMasterConsumer(object):
|
|
|
dc8c34 |
+ def __init__(self, master, consumer):
|
|
|
dc8c34 |
+ master.open()
|
|
|
dc8c34 |
+ self.master = master
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ consumer.open()
|
|
|
dc8c34 |
+ self.consumer = consumer
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ def __repr__(self):
|
|
|
dc8c34 |
+ return "Master[%s] -> Consumer[%s" % (self.master, self.consumer)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+@pytest.fixture(scope="module")
|
|
|
dc8c34 |
+def topology(request):
|
|
|
dc8c34 |
+ '''
|
|
|
dc8c34 |
+ This fixture is used to create a replicated topology for the 'module'.
|
|
|
dc8c34 |
+ The replicated topology is MASTER -> Consumer.
|
|
|
dc8c34 |
+ At the beginning, It may exists a master instance and/or a consumer instance.
|
|
|
dc8c34 |
+ It may also exists a backup for the master and/or the consumer.
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ Principle:
|
|
|
dc8c34 |
+ If master instance exists:
|
|
|
dc8c34 |
+ restart it
|
|
|
dc8c34 |
+ If consumer instance exists:
|
|
|
dc8c34 |
+ restart it
|
|
|
dc8c34 |
+ If backup of master AND backup of consumer exists:
|
|
|
dc8c34 |
+ create or rebind to consumer
|
|
|
dc8c34 |
+ create or rebind to master
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ restore master from backup
|
|
|
dc8c34 |
+ restore consumer from backup
|
|
|
dc8c34 |
+ else:
|
|
|
dc8c34 |
+ Cleanup everything
|
|
|
dc8c34 |
+ remove instances
|
|
|
dc8c34 |
+ remove backups
|
|
|
dc8c34 |
+ Create instances
|
|
|
dc8c34 |
+ Initialize replication
|
|
|
dc8c34 |
+ Create backups
|
|
|
dc8c34 |
+ '''
|
|
|
dc8c34 |
+ global installation_prefix
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ if installation_prefix:
|
|
|
dc8c34 |
+ args_instance[SER_DEPLOYED_DIR] = installation_prefix
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ master = DirSrv(verbose=False)
|
|
|
dc8c34 |
+ consumer = DirSrv(verbose=False)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Args for the master instance
|
|
|
dc8c34 |
+ args_instance[SER_HOST] = HOST_MASTER
|
|
|
dc8c34 |
+ args_instance[SER_PORT] = PORT_MASTER
|
|
|
dc8c34 |
+ args_instance[SER_SERVERID_PROP] = SERVERID_MASTER
|
|
|
dc8c34 |
+ args_master = args_instance.copy()
|
|
|
dc8c34 |
+ master.allocate(args_master)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Args for the consumer instance
|
|
|
dc8c34 |
+ args_instance[SER_HOST] = HOST_CONSUMER
|
|
|
dc8c34 |
+ args_instance[SER_PORT] = PORT_CONSUMER
|
|
|
dc8c34 |
+ args_instance[SER_SERVERID_PROP] = SERVERID_CONSUMER
|
|
|
dc8c34 |
+ args_consumer = args_instance.copy()
|
|
|
dc8c34 |
+ consumer.allocate(args_consumer)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Get the status of the backups
|
|
|
dc8c34 |
+ backup_master = master.checkBackupFS()
|
|
|
dc8c34 |
+ backup_consumer = consumer.checkBackupFS()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Get the status of the instance and restart it if it exists
|
|
|
dc8c34 |
+ instance_master = master.exists()
|
|
|
dc8c34 |
+ if instance_master:
|
|
|
dc8c34 |
+ master.stop(timeout=10)
|
|
|
dc8c34 |
+ master.start(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ instance_consumer = consumer.exists()
|
|
|
dc8c34 |
+ if instance_consumer:
|
|
|
dc8c34 |
+ consumer.stop(timeout=10)
|
|
|
dc8c34 |
+ consumer.start(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ if backup_master and backup_consumer:
|
|
|
dc8c34 |
+ # The backups exist, assuming they are correct
|
|
|
dc8c34 |
+ # we just re-init the instances with them
|
|
|
dc8c34 |
+ if not instance_master:
|
|
|
dc8c34 |
+ master.create()
|
|
|
dc8c34 |
+ # Used to retrieve configuration information (dbdir, confdir...)
|
|
|
dc8c34 |
+ master.open()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ if not instance_consumer:
|
|
|
dc8c34 |
+ consumer.create()
|
|
|
dc8c34 |
+ # Used to retrieve configuration information (dbdir, confdir...)
|
|
|
dc8c34 |
+ consumer.open()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # restore master from backup
|
|
|
dc8c34 |
+ master.stop(timeout=10)
|
|
|
dc8c34 |
+ master.restoreFS(backup_master)
|
|
|
dc8c34 |
+ master.start(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # restore consumer from backup
|
|
|
dc8c34 |
+ consumer.stop(timeout=10)
|
|
|
dc8c34 |
+ consumer.restoreFS(backup_consumer)
|
|
|
dc8c34 |
+ consumer.start(timeout=10)
|
|
|
dc8c34 |
+ else:
|
|
|
dc8c34 |
+ # We should be here only in two conditions
|
|
|
dc8c34 |
+ # - This is the first time a test involve master-consumer
|
|
|
dc8c34 |
+ # so we need to create everything
|
|
|
dc8c34 |
+ # - Something weird happened (instance/backup destroyed)
|
|
|
dc8c34 |
+ # so we discard everything and recreate all
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Remove all the backups. So even if we have a specific backup file
|
|
|
dc8c34 |
+ # (e.g backup_master) we clear all backups that an instance my have created
|
|
|
dc8c34 |
+ if backup_master:
|
|
|
dc8c34 |
+ master.clearBackupFS()
|
|
|
dc8c34 |
+ if backup_consumer:
|
|
|
dc8c34 |
+ consumer.clearBackupFS()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Remove all the instances
|
|
|
dc8c34 |
+ if instance_master:
|
|
|
dc8c34 |
+ master.delete()
|
|
|
dc8c34 |
+ if instance_consumer:
|
|
|
dc8c34 |
+ consumer.delete()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Create the instances
|
|
|
dc8c34 |
+ master.create()
|
|
|
dc8c34 |
+ master.open()
|
|
|
dc8c34 |
+ consumer.create()
|
|
|
dc8c34 |
+ consumer.open()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ #
|
|
|
dc8c34 |
+ # Now prepare the Master-Consumer topology
|
|
|
dc8c34 |
+ #
|
|
|
dc8c34 |
+ # First Enable replication
|
|
|
dc8c34 |
+ master.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER)
|
|
|
dc8c34 |
+ consumer.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_CONSUMER)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Initialize the supplier->consumer
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ properties = {RA_NAME: r'meTo_$host:$port',
|
|
|
dc8c34 |
+ RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
|
|
|
dc8c34 |
+ RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
|
|
|
dc8c34 |
+ RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
|
|
|
dc8c34 |
+ RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
|
|
|
dc8c34 |
+ repl_agreement = master.agreement.create(suffix=SUFFIX, host=consumer.host, port=consumer.port, properties=properties)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ if not repl_agreement:
|
|
|
dc8c34 |
+ log.fatal("Fail to create a replica agreement")
|
|
|
dc8c34 |
+ sys.exit(1)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ log.debug("%s created" % repl_agreement)
|
|
|
dc8c34 |
+ master.agreement.init(SUFFIX, HOST_CONSUMER, PORT_CONSUMER)
|
|
|
dc8c34 |
+ master.waitForReplInit(repl_agreement)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Check replication is working fine
|
|
|
dc8c34 |
+ master.add_s(Entry((TEST_REPL_DN, {
|
|
|
dc8c34 |
+ 'objectclass': "top person".split(),
|
|
|
dc8c34 |
+ 'sn': 'test_repl',
|
|
|
dc8c34 |
+ 'cn': 'test_repl'})))
|
|
|
dc8c34 |
+ loop = 0
|
|
|
dc8c34 |
+ while loop <= 10:
|
|
|
dc8c34 |
+ try:
|
|
|
dc8c34 |
+ ent = consumer.getEntry(TEST_REPL_DN, ldap.SCOPE_BASE, "(objectclass=*)")
|
|
|
dc8c34 |
+ break
|
|
|
dc8c34 |
+ except ldap.NO_SUCH_OBJECT:
|
|
|
dc8c34 |
+ time.sleep(1)
|
|
|
dc8c34 |
+ loop += 1
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Time to create the backups
|
|
|
dc8c34 |
+ master.stop(timeout=10)
|
|
|
dc8c34 |
+ master.backupfile = master.backupFS()
|
|
|
dc8c34 |
+ master.start(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ consumer.stop(timeout=10)
|
|
|
dc8c34 |
+ consumer.backupfile = consumer.backupFS()
|
|
|
dc8c34 |
+ consumer.start(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ #
|
|
|
dc8c34 |
+ # Here we have two instances master and consumer
|
|
|
dc8c34 |
+ # with replication working. Either coming from a backup recovery
|
|
|
dc8c34 |
+ # or from a fresh (re)init
|
|
|
dc8c34 |
+ # Time to return the topology
|
|
|
dc8c34 |
+ return TopologyMasterConsumer(master, consumer)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+def test_ticket47619_init(topology):
|
|
|
dc8c34 |
+ """
|
|
|
dc8c34 |
+ Initialize the test environment
|
|
|
dc8c34 |
+ """
|
|
|
dc8c34 |
+ topology.master.plugins.enable(name=PLUGIN_RETRO_CHANGELOG)
|
|
|
dc8c34 |
+ #topology.master.plugins.enable(name=PLUGIN_MEMBER_OF)
|
|
|
dc8c34 |
+ #topology.master.plugins.enable(name=PLUGIN_REFER_INTEGRITY)
|
|
|
dc8c34 |
+ topology.master.stop(timeout=10)
|
|
|
dc8c34 |
+ topology.master.start(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ topology.master.log.info("test_ticket47619_init topology %r" % (topology))
|
|
|
dc8c34 |
+ # the test case will check if a warning message is logged in the
|
|
|
dc8c34 |
+ # error log of the supplier
|
|
|
dc8c34 |
+ topology.master.errorlog_file = open(topology.master.errlog, "r")
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # add dummy entries
|
|
|
dc8c34 |
+ for cpt in range(MAX_OTHERS):
|
|
|
dc8c34 |
+ name = "%s%d" % (OTHER_NAME, cpt)
|
|
|
dc8c34 |
+ topology.master.add_s(Entry(("cn=%s,%s" % (name, SUFFIX), {
|
|
|
dc8c34 |
+ 'objectclass': "top person".split(),
|
|
|
dc8c34 |
+ 'sn': name,
|
|
|
dc8c34 |
+ 'cn': name})))
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ topology.master.log.info("test_ticket47619_init: %d entries ADDed %s[0..%d]" % (MAX_OTHERS, OTHER_NAME, MAX_OTHERS-1))
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # Check the number of entries in the retro changelog
|
|
|
dc8c34 |
+ time.sleep(2)
|
|
|
dc8c34 |
+ ents = topology.master.search_s(RETROCL_SUFFIX, ldap.SCOPE_ONELEVEL, "(objectclass=*)")
|
|
|
dc8c34 |
+ assert len(ents) == MAX_OTHERS
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+def test_ticket47619_create_index(topology):
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ args = {INDEX_TYPE: 'eq'}
|
|
|
dc8c34 |
+ for attr in ATTRIBUTES:
|
|
|
dc8c34 |
+ topology.master.index.create(suffix=RETROCL_SUFFIX, attr=attr, args=args)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+def test_ticket47619_reindex(topology):
|
|
|
dc8c34 |
+ '''
|
|
|
dc8c34 |
+ Reindex all the attributes in ATTRIBUTES
|
|
|
dc8c34 |
+ '''
|
|
|
dc8c34 |
+ args = {TASK_WAIT: True,
|
|
|
dc8c34 |
+ TASK_TIMEOUT: 10}
|
|
|
dc8c34 |
+ for attr in ATTRIBUTES:
|
|
|
dc8c34 |
+ rc = topology.master.tasks.reindex(suffix=RETROCL_SUFFIX, attrname=attr, args=args)
|
|
|
dc8c34 |
+ assert rc == 0
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+def test_ticket47619_check_indexed_search(topology):
|
|
|
dc8c34 |
+ for attr in ATTRIBUTES:
|
|
|
dc8c34 |
+ ents = topology.master.search_s(RETROCL_SUFFIX, ldap.SCOPE_SUBTREE, "(%s=hello)" % attr)
|
|
|
dc8c34 |
+ assert len(ents) == 0
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+def test_ticket47619_final(topology):
|
|
|
dc8c34 |
+ topology.master.stop(timeout=10)
|
|
|
dc8c34 |
+ topology.consumer.stop(timeout=10)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+def run_isolated():
|
|
|
dc8c34 |
+ '''
|
|
|
dc8c34 |
+ run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..)
|
|
|
dc8c34 |
+ To run isolated without py.test, you need to
|
|
|
dc8c34 |
+ - edit this file and comment '@pytest.fixture' line before 'topology' function.
|
|
|
dc8c34 |
+ - set the installation prefix
|
|
|
dc8c34 |
+ - run this program
|
|
|
dc8c34 |
+ '''
|
|
|
dc8c34 |
+ global installation_prefix
|
|
|
dc8c34 |
+ installation_prefix = None
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ topo = topology(True)
|
|
|
dc8c34 |
+ test_ticket47619_init(topo)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ test_ticket47619_create_index(topo)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ # important restart that trigger the hang
|
|
|
dc8c34 |
+ # at restart, finding the new 'changelog' backend, the backend is acquired in Read
|
|
|
dc8c34 |
+ # preventing the reindex task to complete
|
|
|
dc8c34 |
+ topo.master.restart(timeout=10)
|
|
|
dc8c34 |
+ test_ticket47619_reindex(topo)
|
|
|
dc8c34 |
+ test_ticket47619_check_indexed_search(topo)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ test_ticket47619_final(topo)
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+if __name__ == '__main__':
|
|
|
dc8c34 |
+ run_isolated()
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
diff --git a/ldap/servers/plugins/retrocl/retrocl.c b/ldap/servers/plugins/retrocl/retrocl.c
|
|
|
dc8c34 |
index 08484c7..6ed9fe9 100644
|
|
|
dc8c34 |
--- a/ldap/servers/plugins/retrocl/retrocl.c
|
|
|
dc8c34 |
+++ b/ldap/servers/plugins/retrocl/retrocl.c
|
|
|
dc8c34 |
@@ -239,8 +239,6 @@ static int retrocl_select_backend(void)
|
|
|
dc8c34 |
err = slapi_mapping_tree_select(pb,&be,&referral,errbuf);
|
|
|
dc8c34 |
slapi_entry_free(referral);
|
|
|
dc8c34 |
|
|
|
dc8c34 |
- operation_free(&op,NULL);
|
|
|
dc8c34 |
-
|
|
|
dc8c34 |
if (err != LDAP_SUCCESS || be == NULL || be == defbackend_get_backend()) {
|
|
|
dc8c34 |
LDAPDebug2Args(LDAP_DEBUG_TRACE,"Mapping tree select failed (%d) %s.\n",
|
|
|
dc8c34 |
err,errbuf);
|
|
|
dc8c34 |
@@ -257,6 +255,10 @@ static int retrocl_select_backend(void)
|
|
|
dc8c34 |
}
|
|
|
dc8c34 |
|
|
|
dc8c34 |
retrocl_create_cle();
|
|
|
dc8c34 |
+ slapi_pblock_destroy(pb);
|
|
|
dc8c34 |
+
|
|
|
dc8c34 |
+ if (be)
|
|
|
dc8c34 |
+ slapi_be_Unlock(be);
|
|
|
dc8c34 |
|
|
|
dc8c34 |
return retrocl_get_changenumbers();
|
|
|
dc8c34 |
}
|
|
|
dc8c34 |
--
|
|
|
dc8c34 |
2.4.11
|
|
|
dc8c34 |
|