[ 
https://issues.apache.org/jira/browse/DAFFODIL-3093?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Olabusayo Kilo updated DAFFODIL-3093:
-------------------------------------
    Description: 
_DirectOrBufferedDataOutputStream_ models a forward-only linked chain via a 
private _{_}following: Maybe[DirectOrBufferedDataOutputStream]{_} field: 
splitting a stream ({_}addBuffered(){_}/{_}addBufferedDOS{_}) sets 
_{_}following{_} on the *old* node to point at the *new* node, and every call 
site that performs a split immediately re-points the owning {_}UState{_}'s 
"current" DOS reference ({_}setDataOutputStream{_}) at that new node in the 
same step (see {_}Suspension.scala{_}'s {_}prepareToSuspend{_}/{_}splitDOS{_}, 
{_}LayeredSequenceUnparser.scala{_}, {_}UState.scala{_}'s bit-order-change 
split, {_}BlobLengthUnparser.scala{_}). As a result, the state's "current" 
pointer always tracks the newest node in the chain which itself never has 
_{_}following{_} set (nothing has split off of it yet).

_cleanUp()_ ({_}DirectOrBufferedDataOutputStream.scala{_}) is only ever invoked 
via that same "current" pointer, from two call sites in 
{_}DataProcessor.scala{_}'s _finally_ blocks ({_}unparseSinglePass{_}, 
{_}unparseViaBuildThenWrite{_}). It does two things:
1. Deletes _this_ object's own temp file, if it is currently buffering to one.
2. Recurses forward through _{_}following{_}, repeating step 1 on each 
successor.

Because _cleanUp()_ always starts at the newest, and _{_}following{_} only 
points forward, step 2 can never reach a node *before* the newest in the chain. 
In the normal/successful-completion case this doesn't matter: as each 
suspension resolves, {_}setFinished(){_}'s own forward-merge cascade 
({_}deliverContent{_}, which copies a buffered node's bytes into the direct 
stream and deletes its temp file as part of that) collapses the whole chain 
down to one node before _cleanUp()_ ever runs. But if the unparse aborts 
*before* every pending split has been merged, ex. two or more suspensions are 
permanently blocked (a genuine deadlock), any split that (a) already spilled to 
a temp file and (b) is not the newest and was never merged, is unreachable by 
either mechanism. Its temp file is simply never deleted by Daffodil; it 
survives only because _File.createTempFile(...).deleteOnExit()_ happens to be 
set at file-creation time ({_}ByteArrayOrFileOutputStream.checkBuffer{_}), 
which cleans it up at JVM exit, not at unparse-failure time.


To reproduce:

Schema with two _dfdl:outputValueCalc_ elements, each referencing a distinct 
_dfdl:defineVariable_ that has no default value and is never set (so each 
creates a _Suspension_ that can never resolve , a real, permanent deadlock, not 
a timing artifact), with ordinary content in between:

{code:xml}
<dfdl:defineVariable name="neverSet1" type="xs:int"/>
<dfdl:defineVariable name="neverSet2" type="xs:int"/>
...
<xs:element name="computed1" type="xs:int" dfdl:lengthKind="explicit" 
dfdl:length="4"
dfdl:outputValueCalc="{ $tns:neverSet1 }"/>
<xs:element name="middle" type="xs:string" dfdl:lengthKind="explicit" 
dfdl:length="40"/>
<xs:element name="computed2" type="xs:int" dfdl:lengthKind="explicit" 
dfdl:length="4"
dfdl:outputValueCalc="{ $tns:neverSet2 }"/>
<xs:element name="filler" type="xs:string" dfdl:lengthKind="explicit" 
dfdl:length="40"/>
{code}


Compiled with _maxByteArrayOutputStreamBufferSizeInBytes_ set very small (e.g. 
_1_) so that both buffered splits (behind _computed1_ and behind _computed2_) 
immediately spill to temp files instead of staying in memory, and 
_tempFilePath_ pointed at a scratch directory so leftover files are easy to 
observe.


Unparsing this infoset:
1. Correctly fails with _SuspensionDeadlockException_ ("Expressions/Unparsers 
are circularly deadlocked").
2. Creates two temp files (one behind _computed1_, holding _middle_'s bytes; 
one behind _computed2_, holding _filler_'s bytes, the newest at the time of 
failure).
3. Deletes exactly one of them (the newest's, via _cleanUp()_'s local check). 
The other (behind _computed1_) is left on disk.

  was:
_DirectOrBufferedDataOutputStream_ models a forward-only linked chain via a
private _{_}following: Maybe[DirectOrBufferedDataOutputStream]{_} field: 
splitting
a stream ({_}addBuffered(){_}/{_}addBufferedDOS{_}) sets _{_}following{_} on 
the *old*
node to point at the *new* node, and every call site that performs a split
immediately re-points the owning {_}UState{_}'s "current" DOS reference
({_}setDataOutputStream{_}) at that new node in the same step (see
{_}Suspension.scala{_}'s {_}prepareToSuspend{_}/{_}splitDOS{_}, 
{_}LayeredSequenceUnparser.scala{_},
{_}UState.scala{_}'s bit-order-change split, {_}BlobLengthUnparser.scala{_}). 
As a
result, the state's "current" pointer always tracks the newest node in the
chain which itself never has _{_}following{_} set (nothing has
split off of it yet).

_cleanUp()_ ({_}DirectOrBufferedDataOutputStream.scala{_}) is only
ever invoked via that same "current" pointer, from two call sites in
{_}DataProcessor.scala{_}'s _finally_ blocks ({_}unparseSinglePass{_},
{_}unparseViaBuildThenWrite{_}). It does two things:
1. Deletes _this_ object's own temp file, if it is currently buffering to one.
2. Recurses forward through _{_}following{_}, repeating step 1 on each 
successor.

Because _cleanUp()_ always starts at the newest, and _{_}following{_} only
points forward, step 2 can never reach a node *before* the newest in the
chain. In the normal/successful-completion case this doesn't matter: as each
suspension resolves, {_}setFinished(){_}'s own forward-merge cascade
({_}deliverContent{_}, which copies a buffered node's bytes into the direct
stream and deletes its temp file as part of that) collapses the whole chain
down to one node before _cleanUp()_ ever runs. But if the unparse aborts
*before* every pending split has been merged, ex. two or more suspensions
are permanently blocked (a genuine deadlock), any split that (a) already
spilled to a temp file and (b) is not the newest and was never merged, is
unreachable by either mechanism. Its temp file is simply never deleted by
Daffodil; it survives only because _File.createTempFile(...).deleteOnExit()_
happens to be set at file-creation time
({_}ByteArrayOrFileOutputStream.checkBuffer{_}), which cleans it up at JVM exit,
not at unparse-failure time.


To reproduce:

Schema with two _dfdl:outputValueCalc_ elements, each referencing a distinct
_dfdl:defineVariable_ that has no default value and is never set (so each
creates a _Suspension_ that can never resolve , a real, permanent deadlock,
not a timing artifact), with ordinary content in between:

 {code:xml}
<dfdl:defineVariable name="neverSet1" type="xs:int"/>
<dfdl:defineVariable name="neverSet2" type="xs:int"/>
...
<xs:element name="computed1" type="xs:int" dfdl:lengthKind="explicit" 
dfdl:length="4"
dfdl:outputValueCalc="{ $tns:neverSet1 }"/>
<xs:element name="middle" type="xs:string" dfdl:lengthKind="explicit" 
dfdl:length="40"/>
<xs:element name="computed2" type="xs:int" dfdl:lengthKind="explicit" 
dfdl:length="4"
dfdl:outputValueCalc="{ $tns:neverSet2 }"/>
<xs:element name="filler" type="xs:string" dfdl:lengthKind="explicit" 
dfdl:length="40"/>
{code}


Compiled with _maxByteArrayOutputStreamBufferSizeInBytes_ set very small
(e.g. _1_) so that both buffered splits (behind _computed1_ and behind
_computed2_) immediately spill to temp files instead of staying in memory,
and _tempFilePath_ pointed at a scratch directory so leftover files are easy
to observe.


Unparsing this infoset:
1. Correctly fails with _SuspensionDeadlockException_ ("Expressions/Unparsers
are circularly deadlocked").
2. Creates two temp files (one behind _computed1_, holding _middle_'s bytes;
one behind _computed2_, holding _filler_'s bytes, the newest at the
time of failure).
3. Deletes exactly one of them (the newest's, via _cleanUp()_'s local
check). The other (behind _computed1_) is left on disk.


> DirectOrBufferedDataOutputStream.cleanUp() cannot reach temp files from 
> upstream, un-merged buffered splits
> -----------------------------------------------------------------------------------------------------------
>
>                 Key: DAFFODIL-3093
>                 URL: https://issues.apache.org/jira/browse/DAFFODIL-3093
>             Project: Daffodil
>          Issue Type: Bug
>          Components: Back End, Performance
>    Affects Versions: 4.2.0
>            Reporter: Olabusayo Kilo
>            Priority: Minor
>
> _DirectOrBufferedDataOutputStream_ models a forward-only linked chain via a 
> private _{_}following: Maybe[DirectOrBufferedDataOutputStream]{_} field: 
> splitting a stream ({_}addBuffered(){_}/{_}addBufferedDOS{_}) sets 
> _{_}following{_} on the *old* node to point at the *new* node, and every call 
> site that performs a split immediately re-points the owning {_}UState{_}'s 
> "current" DOS reference ({_}setDataOutputStream{_}) at that new node in the 
> same step (see {_}Suspension.scala{_}'s 
> {_}prepareToSuspend{_}/{_}splitDOS{_}, {_}LayeredSequenceUnparser.scala{_}, 
> {_}UState.scala{_}'s bit-order-change split, {_}BlobLengthUnparser.scala{_}). 
> As a result, the state's "current" pointer always tracks the newest node in 
> the chain which itself never has _{_}following{_} set (nothing has split off 
> of it yet).
> _cleanUp()_ ({_}DirectOrBufferedDataOutputStream.scala{_}) is only ever 
> invoked via that same "current" pointer, from two call sites in 
> {_}DataProcessor.scala{_}'s _finally_ blocks ({_}unparseSinglePass{_}, 
> {_}unparseViaBuildThenWrite{_}). It does two things:
> 1. Deletes _this_ object's own temp file, if it is currently buffering to one.
> 2. Recurses forward through _{_}following{_}, repeating step 1 on each 
> successor.
> Because _cleanUp()_ always starts at the newest, and _{_}following{_} only 
> points forward, step 2 can never reach a node *before* the newest in the 
> chain. In the normal/successful-completion case this doesn't matter: as each 
> suspension resolves, {_}setFinished(){_}'s own forward-merge cascade 
> ({_}deliverContent{_}, which copies a buffered node's bytes into the direct 
> stream and deletes its temp file as part of that) collapses the whole chain 
> down to one node before _cleanUp()_ ever runs. But if the unparse aborts 
> *before* every pending split has been merged, ex. two or more suspensions are 
> permanently blocked (a genuine deadlock), any split that (a) already spilled 
> to a temp file and (b) is not the newest and was never merged, is unreachable 
> by either mechanism. Its temp file is simply never deleted by Daffodil; it 
> survives only because _File.createTempFile(...).deleteOnExit()_ happens to be 
> set at file-creation time ({_}ByteArrayOrFileOutputStream.checkBuffer{_}), 
> which cleans it up at JVM exit, not at unparse-failure time.
> To reproduce:
> Schema with two _dfdl:outputValueCalc_ elements, each referencing a distinct 
> _dfdl:defineVariable_ that has no default value and is never set (so each 
> creates a _Suspension_ that can never resolve , a real, permanent deadlock, 
> not a timing artifact), with ordinary content in between:
> {code:xml}
> <dfdl:defineVariable name="neverSet1" type="xs:int"/>
> <dfdl:defineVariable name="neverSet2" type="xs:int"/>
> ...
> <xs:element name="computed1" type="xs:int" dfdl:lengthKind="explicit" 
> dfdl:length="4"
> dfdl:outputValueCalc="{ $tns:neverSet1 }"/>
> <xs:element name="middle" type="xs:string" dfdl:lengthKind="explicit" 
> dfdl:length="40"/>
> <xs:element name="computed2" type="xs:int" dfdl:lengthKind="explicit" 
> dfdl:length="4"
> dfdl:outputValueCalc="{ $tns:neverSet2 }"/>
> <xs:element name="filler" type="xs:string" dfdl:lengthKind="explicit" 
> dfdl:length="40"/>
> {code}
> Compiled with _maxByteArrayOutputStreamBufferSizeInBytes_ set very small 
> (e.g. _1_) so that both buffered splits (behind _computed1_ and behind 
> _computed2_) immediately spill to temp files instead of staying in memory, 
> and _tempFilePath_ pointed at a scratch directory so leftover files are easy 
> to observe.
> Unparsing this infoset:
> 1. Correctly fails with _SuspensionDeadlockException_ ("Expressions/Unparsers 
> are circularly deadlocked").
> 2. Creates two temp files (one behind _computed1_, holding _middle_'s bytes; 
> one behind _computed2_, holding _filler_'s bytes, the newest at the time of 
> failure).
> 3. Deletes exactly one of them (the newest's, via _cleanUp()_'s local check). 
> The other (behind _computed1_) is left on disk.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to