View Issue Details

IDProjectCategoryView StatusLast Update
0009472FPCCompilerpublic2018-10-04 16:11
ReporterAleks VtyurinAssigned ToYuriy Sydorov 
PrioritynormalSeveritymajorReproducibilityalways
Status closedResolutionwon't fix 
Product Version2.2.0Product Build 
Target VersionFixed in Version 
Summary0009472: "as" increase .RefCount in INTERFACE
DescriptionWhen use "as" for interface typecasts ".RefCount" increase.
Additional Informationvar
 obj: TInterfacedObject;
 iobj, iobj2: IUnknown;
begin
 obj:= TInterfacedObject.Create;
 WriteLn('obj.RefCount=', obj.RefCount); // =0
 
 iobj:= obj;
 WriteLn('obj.RefCount=', obj.RefCount); // =1
 
 iobj2:= iobj as IUnknown; //Error!!!
 WriteLn('obj.RefCount=', obj.RefCount); // need 2, get 3
 
 iobj2:= nil;
 WriteLn('obj.RefCount=', obj.RefCount); // need 1, get 2
 
 iobj:= nil;
 WriteLn('obj.RefCount=', obj.RefCount); // need 0, get 1
end.
TagsNo tags attached.
Fixed in Revision
FPCOldBugId
FPCTarget
Attached Files

Relationships

related to 0008214 closedFlorian Returning interface from function is inefficient 
has duplicate 0011503 closedJonas Maebe Interfaces returned from functions are not properly released. 
has duplicate 0021460 resolvedJonas Maebe As operator on an interface increment the refcount by two 
has duplicate 0025990 resolvedMichael Van Canneyt "as"-operator on interfaces references temporary variable 
related to 0006035 closedJonas Maebe AV by interface access 
related to 0016578 closedJonas Maebe Implicit create interface references 
related to 0030409 resolvedMichael Van Canneyt FPC delphi mode behaviour does not honour Delphi documented object lifetimes 

Activities

2007-08-21 15:37

 

project1.lpr (637 bytes)

Jonas Maebe

2007-08-30 19:50

manager   ~0014346

There is no leak however. The "as" is implemented as a function, the result of this function is returned in a temp, and this temp is obviously refcounted. The temp is freed at the end of the main program (or function, if you do this inside a function).

I don't know when or how it really matters that the refcount temporarily increases because of this. The same happens if you call a function which returns the interfaced object.

Aleks Vtyurin

2007-08-31 18:56

reporter   ~0014364

Jonas Maebe>The same happens if you call a function which returns the interfaced object.

Yes. Error happens not only with "as", but with any "returns the interfaced object"


Jonas Maebe>The temp is freed at the end of the main program

Maybe need "temp:= nil" after returning?


Example:
-----------------------------
var
 obj: TInterfacedObject;
 iobj, iobj2: IUnknown;
 
 function RunAs: IUnknown;
 begin
  Result:= iobj as IUnknown;
  WriteLn('obj.RefCount=', obj.RefCount); // need 2, get 3
 end;
 
begin
 obj:= TInterfacedObject.Create;
 WriteLn('obj.RefCount=', obj.RefCount); // =0
 
 iobj:= obj;
 WriteLn('obj.RefCount=', obj.RefCount); // =1
 
 iobj2:= RunAs; // Error!!!
 WriteLn('obj.RefCount=', obj.RefCount); // need 2, get 3
 
 iobj2:= nil;
 WriteLn('obj.RefCount=', obj.RefCount); // need 1, get 2
 
 iobj:= nil;
 WriteLn('obj.RefCount=', obj.RefCount); // need 0, get 1

Jonas Maebe

2007-08-31 19:11

manager   ~0014365

I still don't understand why it is an error, since no leaking occurs. I know it's different from Delphi, but that in itself does not make it wrong. Adding all those "temp:=nil" statements implicitly will mainly cause code bloat.

Aleks Vtyurin

2007-09-01 22:56

reporter   ~0014393

1.By logic of "interface" using .RefCount must contain object link count.
At example object link count only 2. If .RefCount reach 3 - it`s an error.

2.By logic of "interface" object must destroy when object link count equal 0.
At example object link count equil 0, but object does not destroy - it`s an error.

3.A compatibility with Delphi and other languages.

Jonas Maebe

2007-09-01 23:09

manager   ~0014395

1. No, the refcount is 3 because of the temps.

2. No, the refcount is 1 because of the temps

3. Does this actually cause errors in real world programs, or does it just look strange because you expected something different?

Vincent Snijders

2007-09-01 23:13

manager   ~0014396

Without the temp := nil; you cannot force the destruction of the object by setting all your references it to nil, because the temp keeps the object referenced.

So, in the current FPC implementation, there is no way to limit the lifetime of an object to only part of a function, procedure or method.

Jonas Maebe

2007-09-02 09:47

manager   ~0014401

My question still stands: is this actually a problem in real world apps? FWIW, you get exactly the same behaviour with ansistrings and any other refcounted type.

Aleks Vtyurin

2007-09-02 10:18

reporter   ~0014402

In my application I manualy control removing object from full object list and destroing by .refcounter.

I was compeled to replace "as" with "QueryInterface" call.

I don`t sure of correct application work now.

Jonas Maebe

2007-09-02 10:34

manager   ~0014403

I personally think you are using a hack which only works by accident in Delphi, because the entire point of refcounting is that you leave everything to the underlying run time system and do not manually interfere. Basing your code on assumptions on how the refcouting actually is implemented is asking for trouble and inherently unsupported.

But thanks for giving a real world example, although I don't agree it is a good reason to change our current implementation and therefore I do not intend to modify the current behaviour.

Aleks Vtyurin

2007-09-02 18:59

reporter   ~0014412

For process .RefCount modification hacking is not necessary :)
Necessary realize own IUnknown.

Ex.:

 TMyInterfacedObject = class(TObject,IUnknown)
 protected
  frefcount : longint;

  function QueryInterface(const iid : tguid;out obj) : longint;stdcall;
  function _AddRef : longint;stdcall; virtual;
  function _Release : longint;stdcall; virtual;
 public
  property RefCount : longint read frefcount;
 end;

Aleks Vtyurin

2007-09-03 07:35

reporter   ~0014424

Last edited: 2007-09-03 12:49

This is memory leak example:

procedure Step(aCounter: integer);
var iobj, iobj2: IUnknown;
begin
 if aCounter<1 then Exit;
 
 iobj:= TInterfacedObject.Create;
 iobj2:= iobj as IUnknown;
 // ... some code
 iobj:= nil;
 iobj2:= nil;
 //Error!! Object did not destroyed
 // ... some code
 Step(aCounter-1);
end;
 
begin
 Step(1000);
end.

Aleks Vtyurin

2007-09-03 07:50

reporter   ~0014425

This is lock error example:

var
 iwr: IMyWriter;
 iswr: IMySuperWriter;
 ird: IMyReader;
begin
 iwr:= TMyWriter.Create('C:\some_file.dat'); //Open file and locking
 iswr:= iwr as IMySuperWriter;
 iswr.Write(AnyData);
 iswr:= nil;
//some code
 iwr:= nil; //I want to unlock file and close

 ird:= TMyReader.Create('C:\some_file.dat'); //Open file
 //Error! Because file is locking
 //...

Jonas Maebe

2007-09-03 13:04

manager   ~0014431

I think that the problem in all of your examples is that you are actually using manual reference counting (you could simply replace the nil assignments with calls to a method which decreases the reference count and then you'd have manual reference counting) on top of automatic reference counting, and are depending on the fact that the automatic reference counting follows you manual reference counting scheme.

Either you use automatic reference counting and make no assumptions about how it works under hood (because that implementation may even change over time, or depending on the underlying run time environment), or you use manual reference counting and you are in full control of everything.

Aleks Vtyurin

2007-09-03 13:44

reporter   ~0014434

The current behaviour FPC bring to error.

I showed of several examples.

My programming methods are not relation to it!

Florian

2007-09-03 14:04

administrator   ~0014436

Ref. counted types require temps which must be cleaning in implicit finalize statements and we consider the placing of these implicit finalize statements as up to the compiler as long as there is no leak and there is no leak in FPC. You may not make any assumptions about the current value of the ref. counter.

The proper way of coding it would be some close method and leave the cleaning up of the variables to the compiler.

Florian

2007-09-03 14:05

administrator   ~0014437

I added a relation with some technical background why we use the temp. currently.

Yuriy Sydorov

2007-09-03 14:28

manager   ~0014439

Is it possible to release temps of refcounted types when the temps are no longer needed?
It will solve this problem and make the code more resource efficient.

For example, currently some huge string data can be left in temp after some string operations even if all real string variables are released already...

Jonas Maebe

2007-09-03 14:57

manager   ~0014442

It is possible, but this would cause code bloat and slow everything down (because of all the extra inserted finalization calls for temps). It's also not easy, because it is pretty difficult (if possible at all) to distinguish between a temp and a local variable or parameter inside an inlined function/procedure.

Yuriy Sydorov

2007-09-03 15:21

manager   ~0014443

Yes, this will add some extra code for complex procedures where temps are actively reused.
But if temps are not reused in a procedure the code size will not grow because temp will be released when no longer needed, instead of end of the procedure.

Ref decrement will be used, not Finalize, and it will not slow down execution.

Jonas Maebe

2007-09-03 15:34

manager   ~0014447

Last edited: 2007-09-03 15:35

Either you always do it, or never, because we currently never know in advance whether a temp will be reused or not (temps are allocated ad hoc, not in a separate pass after code generation). Also, it would make the behaviour even less predictable (although, as explained before, I don't think anyone should make any assumptions whatsoever about this behaviour).

The reason it slows down in the general situation (where temps are reused) is because
1) more code -> worse instruction cache behaviour
2) all ref counting assignment operations call the routine to decrease the reference count of the target before actually assigning (and since we cannot distinguish between variables and temps in all situations, you cannot skip this for temps where that would not be necessary in the alternative scenario -- and while that could also be changed, overall we have been moving towards unifying temps and variables more and more over the year rather than making them more different). This means you get two decref counts instead of one (although the second one will see the target is nil and not do anything further).

Yuriy Sydorov

2007-09-03 16:02

manager   ~0014455

Agree. It was not very good idea :) Just found that Delphi do temps finalization at procedure end as well. Therefore it is not real issue.

This bug with "AS" can be easily fixed by changing fpc_intf_as function to procedure with out parameter (Delphi do it such way).
No temps will be need and less code will be genearated.

Thaddy de Koning

2007-09-03 18:15

reporter   ~0014458

Aleks' latter examples show no sign of reference counting by hand.
I guess the problem may be that the function has a pascal calling convention, hence the callee can't clean up the temp.
If it were using cdecl the caller, rather than the callee could clean up, i.e. refdec the temp after the actual assignment and everything should work as expected.
So I am inclined to agree with Aleks there's an issue here.

Jonas Maebe

2007-09-03 18:34

manager   ~0014461

I consider "assigning nil to all user-assigned references and then expecting that no more references exist" to be manual reference counting (as you are manually counting how many references are left and expect some action to be taken when you think that no references are left anymore).

Automatic reference counting means that you put whatever action needs to be taken at destruction time into the destructor and for the rest fire and forget. You do not make any assumptions about when this destructor will be called, only that when it is called no references exist anymore to the reference counted object (and the run time system should take care of that for you, which it does).

This has nothing to do with C vs Pascal. What you propose about decreasing the reference after the assignment is what Yury proposed initially.

Thaddy de Koning

2007-09-03 18:46

reporter   ~0014462

No, I think it is very much different. In this case the calling convention matters for the compiler, implementation-wise, because this call is/should be hidden from the user.
The problem you have with changing the implementation seems to be the indetermined state of the temp. One way to fix this is having the cdecl calling convention and let the caller part of the code decide if it is ment to be "temp". you can't write the compiler in such a way using a calling convention that behaves inhibited, i.e. pascal or stdcall. That's the whole idea. the "as" operator should refdec the temp instance for interfaces. All others shouldn't. This cannot be achieved with "callee" type conventions as opposed to "caller" type conventions.

Jonas Maebe

2007-09-03 19:26

manager   ~0014463

> No, I think it is very much different. In this case the calling convention matters for the compiler,
> implementation-wise, because this call is/should be hidden from the user.

I disagree it has to be hidden any more than it is currently. As explained, the user should not rely on implementation behaviour when working with features such as reference counting.

> The problem you have with changing the implementation seems to be the indetermined state of the
> temp. One way to fix this is having the cdecl calling convention and let the caller part of the code
> decide if it is ment to be "temp".

No, that is not the problem. It is now already the caller which allocates the temp returned by "as“ (and by any other function).

> you can't write the compiler in such a way using a calling convention that behaves inhibited, i.e.
> pascal or stdcall. That's the whole idea. the "as" operator should refdec the temp instance for
> interfaces. All others shouldn't.

They actually should, according to Aleks. And I agree that would be consistent: if you patch the as-behaviour, there's no reason not to patch the function behaviour, since both lead to problems if you rely on no hidden references existing over the course of multiple statements.

It's just that I disagree with the fact that such hidden references lingering around for a while is a bug.

Aleks Vtyurin

2007-09-03 19:31

reporter   ~0014464

This is example of unforeseeable program behaviour:

var
 iwr: IMyWriter;
 iswr: IMySuperWriter;
begin
 iwr:= TMyWriter.Create('C:\some_file.dat'); //Open file and locking
 if ConditionIsTrue
 the begin
  iswr:= iwr as IMySuperWriter;
  iswr.Write(AnyData);
  iswr:= nil;
 end;
 iwr:= nil; //I want to unlock file and close

 //Is file locked?
 //Maybe locked, maybe unlocked :)

Jonas Maebe

2007-09-03 19:49

manager   ~0014465

As Florian mentioned earlier: if you want to unlock a file and close it, you should add a close method and call that. Automatic reference counting is simply not the right tool for that kind of job, because you have no control over how it works and when it does what.

Aleks Vtyurin

2007-09-03 19:53

reporter   ~0014466

Last edited: 2007-09-04 08:02

"iobj:= iobj as IUnknown;" where "iobj=nil" is temporary problem solution.

Ex.:

var
 obj: TInterfacedObject;
 iobj, iobj2: IUnknown;
begin
 obj:= TInterfacedObject.Create;
 WriteLn('obj.RefCount=', obj.RefCount); // =0
 
 iobj:= obj;
 WriteLn('obj.RefCount=', obj.RefCount); // =1
 
 iobj2:= iobj as IUnknown;
 WriteLn('obj.RefCount=', obj.RefCount); // need 2, get 3
 
 iobj2:= nil;
 WriteLn('obj.RefCount=', obj.RefCount); // need 1, get 2
 
 iobj:= nil;
 WriteLn('obj.RefCount=', obj.RefCount); // need 0, get 1

{$IfDef FPC}
 iobj:= iobj as IUnknown; //iTemp:= nil; //Object destroyed!
{$EndIf}

Jonas Maebe

2007-09-03 20:11

manager   ~0014467

I would strongly recommend against relying on this behaviour, because that's (again) just an implementation detail. While we will not change that behaviour just because we can (nor just to annoy you ;), it is not guaranteed to stay like this forever either.

Yuriy Sydorov

2007-09-04 10:20

manager   ~0014493

Ok. I assigned this bug to myself :)
I'll change fpc_intf_as function to procedure with out parameter (Delphi do it such way).
Beside fixing this bug there will be nice "side effects":
No temps will be needed and less code will be genearated for "AS". :)

Jonas Maebe

2007-09-04 10:34

manager   ~0014494

Whatever you change, please make sure that tests/test/trangeob.pp still works afterwards (don't forget to specify the -CR command line option).

Florian

2007-09-04 10:38

administrator   ~0014495

> Whatever you change, please make sure that tests/test/trangeob.pp still works afterwards (don't forget to specify the -CR command line option).

If -CR is critical f0or trangeob, we should duplicate the test using some include.

@Yury: Please see the discussion regarding the related bug. The point is that delphi handles interface function results differently.

Yuriy Sydorov

2007-09-04 11:13

manager   ~0014497

Delphi has simple rule: if function result of interface type is assigned to local variable, temp is not created. Otherwise the temp is created like in FPC currently.

We can implement the same behavior in FPC. Also do not create temps for compilerprocs result (compilerprocs does not modify global variables anyway). fpc_intf_as can be stayed as function in this case.

Florian

2007-09-04 11:19

administrator   ~0014499

IIRC the rule is not that simple. If a function has nested functions it uses a temp or something like this ...

Yuriy Sydorov

2007-09-04 11:24

manager   ~0014500

Delphi does not create temps for nested function results. It check only if destination variable is local and does not create temp if it is true.

P.S. The same rule is used for results of variant type and also can be impemented in FPC to optimize variants.

Jonas Maebe

2007-09-04 11:27

manager   ~0014501

Last edited: 2007-09-07 12:45

Also, if Delphi sometimes creates a temp and sometimes not, Aleks' problem (the one with regular function results being kept in temps) actually also exists in Delphi, but it's just more rarely triggered there.

So I rest my case: it is unsafe to depend on no hidden temps existing anymore after you have manually cleared all your own references, regardless of the implementation.

Yuriy Sydorov

2007-09-04 11:28

manager   ~0014502

Last edited: 2007-09-04 11:34

Oh, actually Delphi checks if _any_ local variables of interface type of parent function is accessed by nested function. And create temp in that case.
It is not hard to do the same...

Jonas Maebe

2007-09-04 11:31

manager   ~0014503

Also: please create a test program with as many "special cases" (nested functions throwing exceptions etc) you can think of, with { %opt=-gh } at the top and "HaltOnNotReleased := true;" at the start, so that the chance of introducing memory leaks is reduced as much as possible.

Yuriy Sydorov

2007-09-04 11:32

manager   ~0014504

Last edited: 2007-09-04 11:33

Jonas, my tests with Delphi shows that it is generally safe to rely that no hidden temps are created for local variables of interface type. But for global variables it is unsafe.

Florian

2007-09-04 11:36

administrator   ~0014505

> Also, if Delphi sometimes creates a temp and sometimes not, Aleks' problem (the one with regular function results being kept in tempts) actually also exists in Delphi, but it's just more rarely triggered there.

Yes, that's why I didn't fix it so far. Delphi's behaviour is rather error prone imo and can be even considered buggy.

Jonas Maebe

2007-09-04 11:55

manager   ~0014507

Yury: something like that cannot be "generally safe". Either it is supported and documented to behave in a particular way, or it is not. Tests do not prove anything, at best they just demonstrate the currently implemented behaviour for the particular cases you tested.

It is just plain wrong to depend on such implementation details of high level concepts. It's the same as depending on the header layout of allocated memory blocks, or even on the code generated for e.g. an inc-statement.

We build a compiler which is compatible to a large extent with the Object Pascal dialect supported by Delphi. We do not build a Delphi code generator emulator (that would be just too limiting for us, not to mention that it would cost a lot of time -- just look at all the energy spent on this bug report).

Yuriy Sydorov

2007-09-04 12:27

manager   ~0014508

Yes. That's why I said "generally safe" instead of "safe" :)

You should agree it will be more efficient to implement Delphi behavior for interfaces and variants - faster code in most cases.

Jonas Maebe

2007-09-04 12:41

manager   ~0014510

Sure, I'm not opposed to optimizations (at least if they don't introduce memory leaks in certain corner cases). But I just want to make clear that the refcounting behaviour that goes with it is a side effect, and not something which is a purpose on its own (nor guaranteed to remain that way, or even be the same on all platforms -- e.g. with an llvm backend, the reference tracking could in theory be done using its garbage collection intrinsics).

Yuriy Sydorov

2007-09-04 13:18

manager   ~0014512

Last edited: 2007-09-04 13:19

Sure. It is dangerous to rely on such internal stuff.
And I agree with Florian that if you want to make sure that interface var is cleaned on specific place of code you need to call some its method to do cleanup (close files, free memory, etc).

By the way we need to document temps allocation for interface/variant/string results to treat "bugs" like this as documented "feature" :)

Peter Vreman

2007-10-16 23:22

administrator   ~0015548

The compiler optimizes now the assignments of interfaces to local variables in procedures. This is the best the compiler can safely do to reduce the refcount increases.

Issue History

Date Modified Username Field Change
2007-08-21 15:37 Aleks Vtyurin New Issue
2007-08-21 15:37 Aleks Vtyurin File Added: project1.lpr
2007-08-30 19:50 Jonas Maebe Note Added: 0014346
2007-08-31 18:56 Aleks Vtyurin Note Added: 0014364
2007-08-31 19:11 Jonas Maebe Note Added: 0014365
2007-09-01 22:56 Aleks Vtyurin Note Added: 0014393
2007-09-01 23:09 Jonas Maebe Note Added: 0014395
2007-09-01 23:13 Vincent Snijders Note Added: 0014396
2007-09-02 09:47 Jonas Maebe Note Added: 0014401
2007-09-02 10:18 Aleks Vtyurin Note Added: 0014402
2007-09-02 10:34 Jonas Maebe Note Added: 0014403
2007-09-02 18:59 Aleks Vtyurin Note Added: 0014412
2007-09-03 07:35 Aleks Vtyurin Note Added: 0014424
2007-09-03 07:50 Aleks Vtyurin Note Added: 0014425
2007-09-03 12:49 Aleks Vtyurin Note Edited: 0014424
2007-09-03 13:04 Jonas Maebe Note Added: 0014431
2007-09-03 13:44 Aleks Vtyurin Note Added: 0014434
2007-09-03 14:04 Florian Note Added: 0014436
2007-09-03 14:04 Florian Relationship added related to 0008214
2007-09-03 14:05 Florian Note Added: 0014437
2007-09-03 14:28 Yuriy Sydorov Note Added: 0014439
2007-09-03 14:57 Jonas Maebe Note Added: 0014442
2007-09-03 15:21 Yuriy Sydorov Note Added: 0014443
2007-09-03 15:34 Jonas Maebe Note Added: 0014447
2007-09-03 15:35 Jonas Maebe Note Edited: 0014447
2007-09-03 16:02 Yuriy Sydorov Note Added: 0014455
2007-09-03 18:15 Thaddy de Koning Note Added: 0014458
2007-09-03 18:34 Jonas Maebe Note Added: 0014461
2007-09-03 18:46 Thaddy de Koning Note Added: 0014462
2007-09-03 19:26 Jonas Maebe Note Added: 0014463
2007-09-03 19:31 Aleks Vtyurin Note Added: 0014464
2007-09-03 19:49 Jonas Maebe Note Added: 0014465
2007-09-03 19:53 Aleks Vtyurin Note Added: 0014466
2007-09-03 19:57 Aleks Vtyurin Note Edited: 0014466
2007-09-03 20:11 Jonas Maebe Note Added: 0014467
2007-09-04 08:02 Aleks Vtyurin Note Edited: 0014466
2007-09-04 10:18 Yuriy Sydorov Status new => assigned
2007-09-04 10:18 Yuriy Sydorov Assigned To => Yuriy Sydorov
2007-09-04 10:20 Yuriy Sydorov Note Added: 0014493
2007-09-04 10:34 Jonas Maebe Note Added: 0014494
2007-09-04 10:38 Florian Note Added: 0014495
2007-09-04 11:13 Yuriy Sydorov Note Added: 0014497
2007-09-04 11:19 Florian Note Added: 0014499
2007-09-04 11:24 Yuriy Sydorov Note Added: 0014500
2007-09-04 11:27 Jonas Maebe Note Added: 0014501
2007-09-04 11:28 Yuriy Sydorov Note Added: 0014502
2007-09-04 11:29 Yuriy Sydorov Note Edited: 0014502
2007-09-04 11:31 Jonas Maebe Note Added: 0014503
2007-09-04 11:32 Yuriy Sydorov Note Added: 0014504
2007-09-04 11:33 Yuriy Sydorov Note Edited: 0014504
2007-09-04 11:34 Yuriy Sydorov Note Edited: 0014502
2007-09-04 11:36 Florian Note Added: 0014505
2007-09-04 11:55 Jonas Maebe Note Added: 0014507
2007-09-04 12:27 Yuriy Sydorov Note Added: 0014508
2007-09-04 12:41 Jonas Maebe Note Added: 0014510
2007-09-04 13:18 Yuriy Sydorov Note Added: 0014512
2007-09-04 13:19 Yuriy Sydorov Note Edited: 0014512
2007-09-07 12:45 Jonas Maebe Note Edited: 0014501
2007-10-16 23:22 Peter Vreman Status assigned => closed
2007-10-16 23:22 Peter Vreman Note Added: 0015548
2007-10-16 23:22 Peter Vreman Resolution open => won't fix
2008-06-19 09:39 Jonas Maebe Relationship added has duplicate 0011503
2009-06-21 11:05 Jonas Maebe Relationship added related to 0006035
2010-05-27 11:49 Jonas Maebe Relationship added related to 0016578
2012-03-13 14:33 Jonas Maebe Relationship added has duplicate 0021460
2014-04-08 17:27 Jonas Maebe Relationship added has duplicate 0025990
2018-10-04 16:11 Michael Van Canneyt Relationship added related to 0030409