Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-54346] && [JENKINS-55505] Fix stream handling in tee. #62

Merged
merged 4 commits into from
Jun 26, 2020

Conversation

tolnaisz
Copy link

Based on work from roel0 this change hopefully fixes the tee issue. It opens the stream only once and closes it at the end of the tee step.

The underlying file gets created and truncated. This is different from the current behavior but I think it is the right one as tee by default (re-)creates the file so it contains only the content tee'd into it.

@dia38
Copy link

dia38 commented Feb 8, 2019

No works for me.

I'm exception java.nio.file.AccessDeniedException:

I'm test on my jenkins infra :

  • master on jenkins 2.150.2 on red hat
  • slave node windows

With it's script and Clean after checkout enable .

pipeline
{
    agent none
    
    stages
    {
        stage('Build 1')
        {
            agent {
                label 'windows'
            }
            steps
            {
                tee('Log1.txt')
                {
                    sh 'ls -la'
                    echo "Build 1"
                    sleep 5
                    echo "Build 2"
                }
            }
        }
        stage('Build 2')
        {
            agent {
                label 'windows'
            }
            steps
            {
                tee('Log2.txt')
                {
                    sh 'ls -la'
                    echo "Build 3"
                    sleep 5
                    echo "Build 4"
                }
            }
        }
    }
}

@tolnaisz
Copy link
Author

tolnaisz commented Feb 8, 2019

dia38, can you please post the whole exception log?

By the way, using 'sh' and 'ls' on a Windows node? Isn't that what causing the issue here?

@dia38
Copy link

dia38 commented Feb 8, 2019

@tolnaisz, yes sh and lson Windows. because git Tool have bash shell

[Pipeline] Start of Pipeline
[Pipeline] stage
[Pipeline] { (Build 1)
[Pipeline] node
Running on windows in c:\jenkins_works\workspace\GHE_NJU_test_tee_master
[Pipeline] {
[Pipeline] checkout
using credential xxxx
Fetching changes from the remote Git repository
Pruning obsolete local branches
Fetching without tags

......

Checking out Revision 91e4337a89e7cac65352339e239269c105348d61 (master)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f 91e4337a89e7cac65352339e239269c105348d61 # timeout=10
 > git branch -a -v --no-abbrev # timeout=10
Commit message: "Rename JenkinsFile to Jenkinsfile"
Cleaning workspace
[Pipeline] withEnv
[Pipeline] {
 > git branch -D master # timeout=10
 > git checkout -b master 91e4337a89e7cac65352339e239269c105348d61 # timeout=10
 > git rev-list --no-walk 91e4337a89e7cac65352339e239269c105348d61 # timeout=10
 > git rev-parse --verify HEAD # timeout=10
Resetting working tree
 > git reset --hard # timeout=10
 > git clean -fdx # timeout=10
[Pipeline] tee
[Pipeline] {
[Pipeline] sh
[Pipeline] }
[Pipeline] // tee
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Build 2)
Stage "Build 2" skipped due to earlier failure(s)
[Pipeline] }
[Pipeline] // stage
[Pipeline] End of Pipeline

GitHub has been notified of this commit’s build result

java.nio.file.AccessDeniedException: /c:\jenkins_works\workspace\GHE_NJU_test_tee_master\Log1.txt
	at sun.nio.fs.UnixException.translateToIOException(UnixException.java:84)
	at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
	at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
	at sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:214)
	at java.nio.file.spi.FileSystemProvider.newOutputStream(FileSystemProvider.java:434)
	at java.nio.file.Files.newOutputStream(Files.java:216)
	at org.jenkinsci.plugins.pipeline.utility.steps.fs.TeeStep$TeeFilter.getStream(TeeStep.java:128)
	at org.jenkinsci.plugins.pipeline.utility.steps.fs.TeeStep$TeeFilter.writeObject(TeeStep.java:155)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1140)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at java.util.ArrayList.writeObject(ArrayList.java:766)
	at sun.reflect.GeneratedMethodAccessor39.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1140)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at hudson.remoting.UserRequest._serialize(UserRequest.java:264)
	at hudson.remoting.UserRequest.serialize(UserRequest.java:273)
	at hudson.remoting.UserRequest.<init>(UserRequest.java:101)
	at hudson.remoting.Channel.call(Channel.java:953)
	at hudson.Launcher$RemoteLauncher.launch(Launcher.java:1059)
	at hudson.plugins.cygpath.CygpathLauncherDecorator$1.launch(CygpathLauncherDecorator.java:66)
	at hudson.Launcher$ProcStarter.start(Launcher.java:455)
	at org.jenkinsci.plugins.durabletask.BourneShellScript.launchWithCookie(BourneShellScript.java:206)
	at org.jenkinsci.plugins.durabletask.FileMonitoringTask.launch(FileMonitoringTask.java:99)
	at org.jenkinsci.plugins.workflow.steps.durable_task.DurableTaskStep$Execution.start(DurableTaskStep.java:305)
	at org.jenkinsci.plugins.workflow.cps.DSL.invokeStep(DSL.java:268)
	at org.jenkinsci.plugins.workflow.cps.DSL.invokeMethod(DSL.java:176)
	at org.jenkinsci.plugins.workflow.cps.CpsScript.invokeMethod(CpsScript.java:122)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
	at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:42)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
	at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:157)
	at org.kohsuke.groovy.sandbox.GroovyInterceptor.onMethodCall(GroovyInterceptor.java:23)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:155)
	at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:155)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:159)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:129)
	at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:129)
	at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:17)
	at WorkflowScript.run(WorkflowScript:16)
	at ___cps.transform___(Native Method)
	at com.cloudbees.groovy.cps.impl.ContinuationGroup.methodCall(ContinuationGroup.java:57)
	at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.dispatchOrArg(FunctionCallBlock.java:109)
	at com.cloudbees.groovy.cps.impl.FunctionCallBlock$ContinuationImpl.fixArg(FunctionCallBlock.java:82)
	at sun.reflect.GeneratedMethodAccessor410.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.cloudbees.groovy.cps.impl.ContinuationPtr$ContinuationImpl.receive(ContinuationPtr.java:72)
	at com.cloudbees.groovy.cps.impl.ConstantBlock.eval(ConstantBlock.java:21)
	at com.cloudbees.groovy.cps.Next.step(Next.java:83)
	at com.cloudbees.groovy.cps.Continuable$1.call(Continuable.java:174)
	at com.cloudbees.groovy.cps.Continuable$1.call(Continuable.java:163)
	at org.codehaus.groovy.runtime.GroovyCategorySupport$ThreadCategoryInfo.use(GroovyCategorySupport.java:129)
	at org.codehaus.groovy.runtime.GroovyCategorySupport.use(GroovyCategorySupport.java:268)
	at com.cloudbees.groovy.cps.Continuable.run0(Continuable.java:163)
	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.access$101(SandboxContinuable.java:34)
	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.lambda$run0$0(SandboxContinuable.java:59)
	at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox.runInSandbox(GroovySandbox.java:121)
	at org.jenkinsci.plugins.workflow.cps.SandboxContinuable.run0(SandboxContinuable.java:58)
	at org.jenkinsci.plugins.workflow.cps.CpsThread.runNextChunk(CpsThread.java:182)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:332)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$200(CpsThreadGroup.java:83)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:244)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:232)
	at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:64)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:131)
	at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
	at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:59)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Finished: FAILURE

@tolnaisz
Copy link
Author

tolnaisz commented Feb 8, 2019

@dia38 No idea. It is working fine for me. Can you please run the same script but without the tee and post the output?

@dia38
Copy link

dia38 commented Feb 8, 2019

@tolnaisz You used API master for create File (on linux) You could see in exception sun.nio.fs.UnixException.translateToIOException(UnixException.java:84) and have / in being windows path /c:\jenkins_works\workspace\GHE_NJU_test_tee_master\Log1.txt.

problem serialization between master on linux, and slave on windows.

@tolnaisz
Copy link
Author

tolnaisz commented Feb 8, 2019

@dia38 Ok, I see the issue now.

@tolnaisz
Copy link
Author

tolnaisz commented Feb 8, 2019

@dia38 Can you please try the new commit?

@dia38
Copy link

dia38 commented Feb 8, 2019

@tolnaisz test success

@tolnaisz
Copy link
Author

@rsandell, @jglick Can you please restart the checks as it looks like it failed due to some issue with the git checkout on the builders.

@@ -63,6 +62,20 @@ public StepExecution start(StepContext context) throws Exception {
return new Execution(context, file);
}

private static final class TeeTail extends BodyExecutionCallback.TailCall {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a serialVersionUID.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, since I lack write permission in this repository, I cannot mark my own review comments as resolved! @github surely that is not by design?

import java.io.OutputStream;
import java.io.Serializable;

import java.io.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a stylistic matter, prefer to revert to explicit imports.


@SuppressWarnings("rawtypes")
@Override
public OutputStream decorateLogger(Run build, final OutputStream logger) throws IOException, InterruptedException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be a lot easier to follow the diff if this were moved back up to where it was.

if (stream == null) {
stream = f.act(new TeeFile(append));
// From now on, when resumed and the stream gets recreated append new data.
append = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would also need to call StepContext.saveState.

return Files.newOutputStream(f.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND/*, StandardOpenOption.DSYNC*/);
} catch (InvalidPathException e) {
throw new IOException(e);
private final Boolean append;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be boolean.

@@ -76,6 +76,47 @@ public void smokes() throws Exception {
});
}

@Test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is verifying a fix for a specific bug, it should be marked with @Issue. Though if I understand correctly, this is just asserting that new tee steps wipe out the previous file contents, which is not a fix for a reported issue. (Whether it is even desirable, I am unsure. I would be inclined to leave the original behavior, since you can always delete the file before running tee if that is what you wanted.)

});
}

@Test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does sound like a test for a fix of JENKINS-54346—of which JENKINS-55505 sounds like a duplicate, though I do not see a test for that specifically (two bat statements inside one tee).


private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
if (ois.readBoolean()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scheme has a minor problem: it will throw an error when a tee step is resumed across an upgrade of the plugin, since it changes the serial form even in program.dat where that must not be done.

The issue could be fixed, I think, by using writeReplace and readResolve rather than writeObject and readObject: when Channel.current() == null, just replace with this; when replacing for Remoting, use a special struct class which just resolves back to a filter with the stream configured.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way these kinds of bugs can generally be reproduced, when anyone wants to bother, in a test using @LocalData. In the case of the tee step that would be hard since the serial form in program.dat encodes an absolute file path, which would be invalid when running the test later. Testing resumption of sh steps suffers from the same problem. JenkinsRule offers no good way around this, though someday I might write a special test-only PickleFactory which replaces any File, Path, or String mentioning a file path starting with $JENKINS_HOME or an agent remoteFS root.

@@ -89,50 +103,92 @@ public boolean start() throws Exception {
private static class TeeFilter extends ConsoleLogFilter implements Serializable {

private final FilePath f;
private boolean append = false; // By default create a new output.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also cause truncation of files when resuming across an upgrade.

@jglick
Copy link
Member

jglick commented Feb 12, 2019

While I think the basic change here is right, it took me a while to get the context. Also the bug fix is entangled with a behavioral change (truncating the file upon starting the step), which not only makes the code more complex but also arguably introduces a regression for scripts not expecting this. That part would be best extracted to a follow-on PR if really needed (as previously noted you can also simply delete any existing file before starting), and made opt-in for compatibility.

So prior to JEP-210 I suppose the only actual problem was an unclosed file handle, which could have been addressed much more simply by stashing a transient stream field in the filter and adding the TailCall to close it: decorateLogger would have been called just once per tee step per master session, even if you were using some steps which remoted their TaskListener—as at that time I think only git (or checkout with GitSCM) did, among commonly used steps.

#51 & #61 introduced tests against JEP-210 mode, in which decorateLogger may be called again for every nested step, and may even be called on the remote side for git or for sh / bat if in USE_WATCHING mode—which until jenkinsci/workflow-durable-task-step-plugin#96 (not yet integrated in this PR) was enabled during tests. Each such call would create a new stream, which would mean that repeated step output could produce errors on Windows, and interleaved step output could produce mangled content on Linux. I am unclear on whether the current tests reproduce these scenarios fully. It would be best if the tests would explicitly simulate both repeated steps and interleaved output, including remoted TaskListeners as with sh / bat in USE_WATCHING mode or a test-only step which does this (there are some examples floating around in various repositories and perhaps the test JAR from workflow-support ought to include one to complement SemaphoreStep).

@jglick
Copy link
Member

jglick commented Feb 12, 2019

Actually I think the lazy initialization of the stream is likely to lead to a subtle bug: if the first nested step output inside tee comes from a remoted TaskListener—as for example sh in USE_WATCHING mode after jenkinsci/durable-task-plugin#83 —and then there is more output from another step (perhaps on the master side), you will get a file lock or corrupted file issue. The master-side filter will initially have a null stream, so the remoted filter will too, but the stream will first be created on the remote side, then this copy of the filter is discarded (since serialization of the stream is only done in the master → agent direction), and then the stream will be created on the master side (or again on the remote side in another copy of the filter).

If true (and this ought to be reproducible in a test), then the fix would be to eagerly initialize the stream in the master-side filter when starting or resuming the step (could use onResume).

@jglick
Copy link
Member

jglick commented Feb 12, 2019

eagerly initialize the stream in the master-side filter

Unfortunately this could make for worse performance when using remote-printing steps, as RemoteOutputStream / ProxyOutputStream are not smart enough to notice¹ that a stream initially constructed on the agent was passed to the master and then back again to the agent—so it will transfer content in both directions before saving it to disk. (The same could happen with the patch as written, depending on circumstances.) At any rate, performance is unlikely important for a relatively obscure step like this.


¹Compare FilePath.writeObject / readObject which does figure out when it can behave as a local file.

@tolnaisz
Copy link
Author

@jglick I din't get your comments around the JEP-210.

I have tried to fix the resume after plug-in upgrade issue. Hopefully it is fixed now. Not sure how to test.

Removed the 'append'-related modifications.

There is no simple way to fix the original implementation just by adding a tail callback. The original version might have opened the stream multiple times.

Regarding the lazy initialization. The stream gets created the first time it is needed either when a local step gets executed and the decorateLogger gets called or when the filter gets transferred to the remote and the writeObject gets called.

Regarding the performance. I don't see a real way around this as, as you also mentioned, the RemoteOutputStream isn't smart enough to notice the unnecessary round trip from remote-to-master-to-remote. We need a way to close the opened stream(s) at the end of the step, so the easiest is to open one stream only and use that across all executions. I don't see other way to close streams opened on the remote side. This means that the stream has to be opened from the master and unavoidably have the data round trip issue.

@tolnaisz
Copy link
Author

Cannot access the details of the build/checks failure. Can somebody fix the link?

@jglick
Copy link
Member

jglick commented Feb 19, 2019

Can somebody fix the link?

Well the link is fine, the server is not!

Copy link
Member

@jglick jglick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the lazy initialization of the stream is likely to lead to a subtle bug

Still appears to be true after the recent changes.

This scheme has a minor problem: it will throw an error when a tee step is resumed across an upgrade of the plugin, since it changes the serial form even in program.dat where that must not be done.

Ditto.

@@ -63,6 +62,20 @@ public StepExecution start(StepContext context) throws Exception {
return new Execution(context, file);
}

private static final class TeeTail extends BodyExecutionCallback.TailCall {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, since I lack write permission in this repository, I cannot mark my own review comments as resolved! @github surely that is not by design?

" echo 'first message'\n" +
" }\n" +
" if (isUnix()) {sh 'rm x.log'} else {bat 'del x.log'}\n" +
" writeFile file: 'x.log', text: 'second message'\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh? What is this testing? You are deleting a file, rewriting it with some new contents, reading those same contents back, and asserting you read what you just wrote?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tests that the file gets closed properly so it can be deleted, rewritten and the new content read back. This test the exact scenario of the JENKINS-54346 bug where the file wasn't closed after the tee step.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reproduce that I think it would suffice to just have the del on Windows, right?

" if (!isUnix()) { bat 'echo second message' }\n" +
" }\n" +
" if (isUnix()) {sh 'rm x.log'} else {bat 'del x.log'}\n" +
" writeFile file: 'x.log', text: 'third message'\n" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here again I am unclear on what exactly is being tested.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test the JENKINS-55505 (I think) ensuring that 2 writes using bat step doesn't lead to the tee output file being kept open. Ensures that it gets closed and can be deleted and overwritten.

@jglick
Copy link
Member

jglick commented Feb 19, 2019

I din't get your comments around the JEP-210.

Not simple to explain I am afraid; that JEP involved a lot of changes, some of which affect this step. I would probably need to take over this PR to get some of the details right. In the meantime it may be better to just avoid the step; certainly you do not need it when the block only involves sh and bat steps, which do their own internal teeing.

@tolnaisz
Copy link
Author

I think the lazy initialization of the stream is likely to lead to a subtle bug

Still appears to be true after the recent changes.

This scheme has a minor problem: it will throw an error when a tee step is resumed across an upgrade of the plugin, since it changes the serial form even in program.dat where that must not be done.

Ditto.

I don't see where the lazy initialization of the stream can cause an issue. It gets initialized always on the master when it is needed and then used on the master or transferred to the remote.

I am not really familiar with the serialization. My assumption is that the object gets created and any field from the serialization stream gets restored. As the transferredToRemote wasn't in the 'old' stream it should be set to false and therefore the readObject succeed without trying to read more data than it should leaving the stream's state as required. Am I missing something?

@jglick
Copy link
Member

jglick commented Feb 20, 2019

It gets initialized always on the master when it is needed

The problem is that the filter may be transferred from master to agent, the copy initialized on the agent, then the original initialized on the master—opening the stream twice. It depends on the behavior of the step using the filter. For example, given USE_WATCHING mode and jenkinsci/durable-task-plugin#83, I suspect

node {
  tee('log') {
    bat 'echo 1'
    echo '2'
  }
}

would fail in this way.

therefore the readObject succeed without trying to read more data than it should

Perhaps. In the absence of a test case (and as I mentioned earlier, a @LocalData test for this step would be hard), it is hard to verify correctness by code review. I tend to use writeReplace / readResolve as a safer and more legible system.

@tolnaisz
Copy link
Author

The problem is that the filter may be transferred from master to agent, the copy initialized on the agent, then the original initialized on the master—opening the stream twice.

To transfer the filter to agent the filter must be serialized to a stream first using writeObject which ensures that the stream gets initialized on the master before the transfer happens.

I tend to use writeReplace / readResolve as a safer and more legible system.

Sorry I am not really sure how to use writeReplace / readResolve. readResolve is a post-action after the object gets initialized from the stream. The transferredToRemote flag supposed to handle this. However the issue is that the transferred stream object needs to be read from the stream as well if it is present. That cannot be done in readResolve leaving no other option than to use readObject.

@jglick
Copy link
Member

jglick commented Mar 1, 2019

writeObject […] ensures that the stream gets initialized on the master

Due to

oos.writeObject(stream = append(f, stream));

I suppose? If so, it would be nice to be able to prove this as sketched in #62 (comment).

readResolve is a post-action after the object gets initialized from the stream.

Right, which is why you use writeReplace to swap out an unrelated Serializable class that is just a struct with the necessary fields and a readResolve method. Thus TeeFilter would be serialized and deserialized exactly as it is now for purposes of program.dat, whereas in the case of agent serialization (Channel.current() != null) you would replace it with a TeeFilterForAgent object with a FilePath and OutputStream, which when loaded on the agent side would produce a TeeFilter with an initialized stream. Again, just a matter of comprehensibility: unlike with the defaultReadObject idiom, it is clear that there are two distinct serial forms and which is used when.

I think this is mergeable; my outstanding concerns are just about style and test coverage. Thank you for your patience with this difficult fix.

@rsandell
Copy link
Member

rsandell commented Mar 7, 2019

@rsandell
Copy link
Member

rsandell commented Apr 8, 2019

Any movement on the test failure?

@PhilippHomann
Copy link
Contributor

@tolnaisz @jglick
Is there anybody who cares about this PR?
Would be nice to use tee with parallel builds..

@jglick
Copy link
Member

jglick commented Nov 22, 2019

Not sure if anyone is actively maintaining this.

@rsandell
Copy link
Member

I've had quite a few drive by contributions where my code reviews haven't been addressed. If anyone wants to take over the PR I would be happy to merge that.

@PhilippHomann
Copy link
Contributor

@rsandell @jglick Opened a followup PR (#77) where the test failure is fixed

@rsandell rsandell merged commit 0a3580c into jenkinsci:master Jun 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants