001/** 002 * Copyright (C) 2012 FuseSource, Inc. 003 * http://fusesource.com 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.fusesource.hawtdispatch.transport; 019 020import org.fusesource.hawtdispatch.*; 021import org.fusesource.hawtdispatch.internal.BaseSuspendable; 022 023import java.io.IOException; 024import java.net.*; 025import java.nio.ByteBuffer; 026import java.nio.channels.*; 027import java.util.LinkedList; 028import java.util.concurrent.Executor; 029import java.util.concurrent.TimeUnit; 030 031/** 032 * An implementation of the {@link org.fusesource.hawtdispatch.transport.Transport} interface using raw tcp/ip 033 * 034 * @author <a href="http://hiramchirino.com">Hiram Chirino</a> 035 */ 036public class TcpTransport extends ServiceBase implements Transport { 037 038 static InetAddress localhost; 039 synchronized static public InetAddress getLocalHost() throws UnknownHostException { 040 // cache it... 041 if( localhost==null ) { 042 // this can be slow on some systems and we use repeatedly. 043 localhost = InetAddress.getLocalHost(); 044 } 045 return localhost; 046 } 047 048 abstract static class SocketState { 049 void onStop(Task onCompleted) { 050 } 051 void onCanceled() { 052 } 053 boolean is(Class<? extends SocketState> clazz) { 054 return getClass()==clazz; 055 } 056 } 057 058 static class DISCONNECTED extends SocketState{} 059 060 class CONNECTING extends SocketState{ 061 void onStop(Task onCompleted) { 062 trace("CONNECTING.onStop"); 063 CANCELING state = new CANCELING(); 064 socketState = state; 065 state.onStop(onCompleted); 066 } 067 void onCanceled() { 068 trace("CONNECTING.onCanceled"); 069 CANCELING state = new CANCELING(); 070 socketState = state; 071 state.onCanceled(); 072 } 073 } 074 075 class CONNECTED extends SocketState { 076 077 public CONNECTED() { 078 localAddress = channel.socket().getLocalSocketAddress(); 079 remoteAddress = channel.socket().getRemoteSocketAddress(); 080 } 081 082 void onStop(Task onCompleted) { 083 trace("CONNECTED.onStop"); 084 CANCELING state = new CANCELING(); 085 socketState = state; 086 state.add(createDisconnectTask()); 087 state.onStop(onCompleted); 088 } 089 void onCanceled() { 090 trace("CONNECTED.onCanceled"); 091 CANCELING state = new CANCELING(); 092 socketState = state; 093 state.add(createDisconnectTask()); 094 state.onCanceled(); 095 } 096 Task createDisconnectTask() { 097 return new Task(){ 098 public void run() { 099 listener.onTransportDisconnected(); 100 } 101 }; 102 } 103 } 104 105 class CANCELING extends SocketState { 106 private LinkedList<Task> runnables = new LinkedList<Task>(); 107 private int remaining; 108 private boolean dispose; 109 110 public CANCELING() { 111 if( readSource!=null ) { 112 remaining++; 113 readSource.cancel(); 114 } 115 if( writeSource!=null ) { 116 remaining++; 117 writeSource.cancel(); 118 } 119 } 120 void onStop(Task onCompleted) { 121 trace("CANCELING.onCompleted"); 122 add(onCompleted); 123 dispose = true; 124 } 125 void add(Task onCompleted) { 126 if( onCompleted!=null ) { 127 runnables.add(onCompleted); 128 } 129 } 130 void onCanceled() { 131 trace("CANCELING.onCanceled"); 132 remaining--; 133 if( remaining!=0 ) { 134 return; 135 } 136 try { 137 if( closeOnCancel ) { 138 channel.close(); 139 } 140 } catch (IOException ignore) { 141 } 142 socketState = new CANCELED(dispose); 143 for (Task runnable : runnables) { 144 runnable.run(); 145 } 146 if (dispose) { 147 dispose(); 148 } 149 } 150 } 151 152 class CANCELED extends SocketState { 153 private boolean disposed; 154 155 public CANCELED(boolean disposed) { 156 this.disposed=disposed; 157 } 158 159 void onStop(Task onCompleted) { 160 trace("CANCELED.onStop"); 161 if( !disposed ) { 162 disposed = true; 163 dispose(); 164 } 165 onCompleted.run(); 166 } 167 } 168 169 protected URI remoteLocation; 170 protected URI localLocation; 171 protected TransportListener listener; 172 protected ProtocolCodec codec; 173 174 protected SocketChannel channel; 175 176 protected SocketState socketState = new DISCONNECTED(); 177 178 protected DispatchQueue dispatchQueue; 179 private DispatchSource readSource; 180 private DispatchSource writeSource; 181 protected CustomDispatchSource<Integer, Integer> drainOutboundSource; 182 protected CustomDispatchSource<Integer, Integer> yieldSource; 183 184 protected boolean useLocalHost = true; 185 186 int maxReadRate; 187 int maxWriteRate; 188 int receiveBufferSize = 1024*64; 189 int sendBufferSize = 1024*64; 190 boolean closeOnCancel = true; 191 192 boolean keepAlive = true; 193 194 public static final int IPTOS_LOWCOST = 0x02; 195 public static final int IPTOS_RELIABILITY = 0x04; 196 public static final int IPTOS_THROUGHPUT = 0x08; 197 public static final int IPTOS_LOWDELAY = 0x10; 198 199 int trafficClass = IPTOS_THROUGHPUT; 200 201 protected RateLimitingChannel rateLimitingChannel; 202 SocketAddress localAddress; 203 SocketAddress remoteAddress; 204 protected Executor blockingExecutor; 205 206 class RateLimitingChannel implements ScatteringByteChannel, GatheringByteChannel { 207 208 int read_allowance = maxReadRate; 209 boolean read_suspended = false; 210// int read_resume_counter = 0; 211 int write_allowance = maxWriteRate; 212 boolean write_suspended = false; 213 214 public void resetAllowance() { 215 if( read_allowance != maxReadRate || write_allowance != maxWriteRate) { 216 read_allowance = maxReadRate; 217 write_allowance = maxWriteRate; 218 if( write_suspended ) { 219 write_suspended = false; 220 resumeWrite(); 221 } 222 if( read_suspended ) { 223 read_suspended = false; 224 resumeRead(); 225 } 226 } 227 } 228 229 public int read(ByteBuffer dst) throws IOException { 230 if( maxReadRate ==0 ) { 231 return channel.read(dst); 232 } else { 233 int rc=0; 234 int reduction = 0; 235 try { 236 int remaining = dst.remaining(); 237 if( read_allowance ==0 || remaining ==0 ) { 238 return 0; 239 } 240 if( remaining > read_allowance) { 241 reduction = remaining - read_allowance; 242 dst.limit(dst.limit() - reduction); 243 } 244 rc = channel.read(dst); 245 read_allowance -= rc; 246 } finally { 247 if( read_allowance<=0 && !read_suspended ) { 248 // we need to suspend the read now until we get 249 // a new allowance.. 250 readSource.suspend(); 251 read_suspended = true; 252 } 253 if( reduction!=0 ) { 254 dst.limit(dst.limit() + reduction); 255 } 256 } 257 return rc; 258 } 259 } 260 261 public int write(ByteBuffer src) throws IOException { 262 if( maxWriteRate ==0 ) { 263 return channel.write(src); 264 } else { 265 int remaining = src.remaining(); 266 if( write_allowance ==0 || remaining ==0 ) { 267 return 0; 268 } 269 270 int reduction = 0; 271 if( remaining > write_allowance) { 272 reduction = remaining - write_allowance; 273 src.limit(src.limit() - reduction); 274 } 275 int rc = 0; 276 try { 277 rc = channel.write(src); 278 write_allowance -= rc; 279 } finally { 280 if( reduction!=0 ) { 281 if( src.remaining() == 0 ) { 282 // we need to suspend the read now until we get 283 // a new allowance.. 284 write_suspended = true; 285 suspendWrite(); 286 } 287 src.limit(src.limit() + reduction); 288 } 289 } 290 return rc; 291 } 292 } 293 294 public boolean isOpen() { 295 return channel.isOpen(); 296 } 297 298 public void close() throws IOException { 299 channel.close(); 300 } 301 302 public void resumeRead() { 303// if( read_suspended ) { 304// read_resume_counter += 1; 305// } else { 306 _resumeRead(); 307// } 308 } 309 310 public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { 311 if(offset+length > dsts.length || length<0 || offset<0) { 312 throw new IndexOutOfBoundsException(); 313 } 314 long rc=0; 315 for (int i = 0; i < length; i++) { 316 ByteBuffer dst = dsts[offset+i]; 317 if(dst.hasRemaining()) { 318 rc += read(dst); 319 } 320 if( dst.hasRemaining() ) { 321 return rc; 322 } 323 } 324 return rc; 325 } 326 327 public long read(ByteBuffer[] dsts) throws IOException { 328 return read(dsts, 0, dsts.length); 329 } 330 331 public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { 332 if(offset+length > srcs.length || length<0 || offset<0) { 333 throw new IndexOutOfBoundsException(); 334 } 335 long rc=0; 336 for (int i = 0; i < length; i++) { 337 ByteBuffer src = srcs[offset+i]; 338 if(src.hasRemaining()) { 339 rc += write(src); 340 } 341 if( src.hasRemaining() ) { 342 return rc; 343 } 344 } 345 return rc; 346 } 347 348 public long write(ByteBuffer[] srcs) throws IOException { 349 return write(srcs, 0, srcs.length); 350 } 351 352 } 353 354 private final Task CANCEL_HANDLER = new Task() { 355 public void run() { 356 socketState.onCanceled(); 357 } 358 }; 359 360 static final class OneWay { 361 final Object command; 362 final Retained retained; 363 364 public OneWay(Object command, Retained retained) { 365 this.command = command; 366 this.retained = retained; 367 } 368 } 369 370 public void connected(SocketChannel channel) throws IOException, Exception { 371 this.channel = channel; 372 initializeChannel(); 373 this.socketState = new CONNECTED(); 374 } 375 376 protected void initializeChannel() throws Exception { 377 this.channel.configureBlocking(false); 378 Socket socket = channel.socket(); 379 try { 380 socket.setReuseAddress(true); 381 } catch (SocketException e) { 382 } 383 try { 384 socket.setSoLinger(true, 0); 385 } catch (SocketException e) { 386 } 387 try { 388 socket.setTrafficClass(trafficClass); 389 } catch (SocketException e) { 390 } 391 try { 392 socket.setKeepAlive(keepAlive); 393 } catch (SocketException e) { 394 } 395 try { 396 socket.setTcpNoDelay(true); 397 } catch (SocketException e) { 398 } 399 try { 400 socket.setReceiveBufferSize(receiveBufferSize); 401 } catch (SocketException e) { 402 } 403 try { 404 socket.setSendBufferSize(sendBufferSize); 405 } catch (SocketException e) { 406 } 407 408 if( channel!=null && codec!=null ) { 409 initializeCodec(); 410 } 411 } 412 413 protected void initializeCodec() throws Exception { 414 codec.setTransport(this); 415 } 416 417 private void initRateLimitingChannel() { 418 if( (maxReadRate !=0 || maxWriteRate !=0) && rateLimitingChannel==null ) { 419 rateLimitingChannel = new RateLimitingChannel(); 420 } 421 } 422 423 public void connecting(final URI remoteLocation, final URI localLocation) throws Exception { 424 this.channel = SocketChannel.open(); 425 initializeChannel(); 426 this.remoteLocation = remoteLocation; 427 this.localLocation = localLocation; 428 socketState = new CONNECTING(); 429 } 430 431 432 public DispatchQueue getDispatchQueue() { 433 return dispatchQueue; 434 } 435 436 public void setDispatchQueue(DispatchQueue queue) { 437 this.dispatchQueue = queue; 438 if(readSource!=null) readSource.setTargetQueue(queue); 439 if(writeSource!=null) writeSource.setTargetQueue(queue); 440 if(drainOutboundSource!=null) drainOutboundSource.setTargetQueue(queue); 441 if(yieldSource!=null) yieldSource.setTargetQueue(queue); 442 } 443 444 public void _start(Task onCompleted) { 445 try { 446 if (socketState.is(CONNECTING.class)) { 447 448 // Resolving host names might block.. so do it on the blocking executor. 449 this.blockingExecutor.execute(new Runnable() { 450 public void run() { 451 try { 452 453 final InetSocketAddress localAddress = (localLocation != null) ? 454 new InetSocketAddress(InetAddress.getByName(localLocation.getHost()), localLocation.getPort()) 455 : null; 456 457 String host = resolveHostName(remoteLocation.getHost()); 458 final InetSocketAddress remoteAddress = new InetSocketAddress(host, remoteLocation.getPort()); 459 460 // Done resolving.. switch back to the dispatch queue. 461 dispatchQueue.execute(new Task() { 462 @Override 463 public void run() { 464 // No need to complete if we have been canceled. 465 if( ! socketState.is(CONNECTING.class) ) { 466 return; 467 } 468 try { 469 470 if (localAddress != null) { 471 channel.socket().bind(localAddress); 472 } 473 trace("connecting..."); 474 channel.connect(remoteAddress); 475 476 // this allows the connect to complete.. 477 readSource = Dispatch.createSource(channel, SelectionKey.OP_CONNECT, dispatchQueue); 478 readSource.setEventHandler(new Task() { 479 public void run() { 480 if (getServiceState() != STARTED) { 481 return; 482 } 483 try { 484 trace("connected."); 485 channel.finishConnect(); 486 readSource.setCancelHandler(null); 487 readSource.cancel(); 488 readSource = null; 489 socketState = new CONNECTED(); 490 onConnected(); 491 } catch (IOException e) { 492 onTransportFailure(e); 493 } 494 } 495 }); 496 readSource.setCancelHandler(CANCEL_HANDLER); 497 readSource.resume(); 498 499 } catch (Exception e) { 500 try { 501 channel.close(); 502 } catch (Exception ignore) { 503 } 504 socketState = new CANCELED(true); 505 if (! (e instanceof IOException)) { 506 e = new IOException(e); 507 } 508 listener.onTransportFailure((IOException)e); 509 } 510 } 511 }); 512 513 } catch (final IOException e) { 514 // we're in blockingExecutor thread context here 515 dispatchQueue.execute(new Task() { 516 public void run() { 517 try { 518 channel.close(); 519 } catch (IOException ignore) { 520 } 521 socketState = new CANCELED(true); 522 listener.onTransportFailure(e); 523 } 524 }); 525 } 526 } 527 }); 528 } else if (socketState.is(CONNECTED.class)) { 529 dispatchQueue.execute(new Task() { 530 public void run() { 531 try { 532 trace("was connected."); 533 onConnected(); 534 } catch (IOException e) { 535 onTransportFailure(e); 536 } 537 } 538 }); 539 } else { 540 trace("cannot be started. socket state is: " + socketState); 541 } 542 } finally { 543 if (onCompleted != null) { 544 onCompleted.run(); 545 } 546 } 547 } 548 549 public void _stop(final Task onCompleted) { 550 trace("stopping.. at state: "+socketState); 551 socketState.onStop(onCompleted); 552 } 553 554 protected String resolveHostName(String host) throws UnknownHostException { 555 if (isUseLocalHost()) { 556 String localName = getLocalHost().getHostName(); 557 if (localName != null && localName.equals(host)) { 558 return "localhost"; 559 } 560 } 561 return host; 562 } 563 564 protected void onConnected() throws IOException { 565 yieldSource = Dispatch.createSource(EventAggregators.INTEGER_ADD, dispatchQueue); 566 yieldSource.setEventHandler(new Task() { 567 public void run() { 568 drainInbound(); 569 } 570 }); 571 yieldSource.resume(); 572 drainOutboundSource = Dispatch.createSource(EventAggregators.INTEGER_ADD, dispatchQueue); 573 drainOutboundSource.setEventHandler(new Task() { 574 public void run() { 575 flush(); 576 } 577 }); 578 drainOutboundSource.resume(); 579 580 readSource = Dispatch.createSource(channel, SelectionKey.OP_READ, dispatchQueue); 581 writeSource = Dispatch.createSource(channel, SelectionKey.OP_WRITE, dispatchQueue); 582 583 readSource.setCancelHandler(CANCEL_HANDLER); 584 writeSource.setCancelHandler(CANCEL_HANDLER); 585 586 readSource.setEventHandler(new Task() { 587 public void run() { 588 drainInbound(); 589 } 590 }); 591 writeSource.setEventHandler(new Task() { 592 public void run() { 593 flush(); 594 } 595 }); 596 597 initRateLimitingChannel(); 598 if( rateLimitingChannel!=null ) { 599 schedualRateAllowanceReset(); 600 } 601 listener.onTransportConnected(); 602 } 603 604 private void schedualRateAllowanceReset() { 605 dispatchQueue.executeAfter(1, TimeUnit.SECONDS, new Task(){ 606 public void run() { 607 if( !socketState.is(CONNECTED.class) ) { 608 return; 609 } 610 rateLimitingChannel.resetAllowance(); 611 schedualRateAllowanceReset(); 612 } 613 }); 614 } 615 616 private void dispose() { 617 if( readSource!=null ) { 618 readSource.cancel(); 619 readSource=null; 620 } 621 622 if( writeSource!=null ) { 623 writeSource.cancel(); 624 writeSource=null; 625 } 626 } 627 628 public void onTransportFailure(IOException error) { 629 listener.onTransportFailure(error); 630 socketState.onCanceled(); 631 } 632 633 634 public boolean full() { 635 return codec==null || 636 codec.full() || 637 !socketState.is(CONNECTED.class) || 638 getServiceState() != STARTED; 639 } 640 641 boolean rejectingOffers; 642 643 public boolean offer(Object command) { 644 dispatchQueue.assertExecuting(); 645 if( full() ) { 646 return false; 647 } 648 try { 649 ProtocolCodec.BufferState rc = codec.write(command); 650 rejectingOffers = codec.full(); 651 switch (rc ) { 652 case FULL: 653 return false; 654 default: 655 drainOutboundSource.merge(1); 656 } 657 } catch (IOException e) { 658 onTransportFailure(e); 659 } 660 return true; 661 } 662 663 boolean writeResumedForCodecFlush = false; 664 665 /** 666 * 667 */ 668 public void flush() { 669 dispatchQueue.assertExecuting(); 670 if (getServiceState() != STARTED || !socketState.is(CONNECTED.class)) { 671 return; 672 } 673 try { 674 if( codec.flush() == ProtocolCodec.BufferState.EMPTY && transportFlush() ) { 675 if( writeResumedForCodecFlush) { 676 writeResumedForCodecFlush = false; 677 suspendWrite(); 678 } 679 rejectingOffers = false; 680 listener.onRefill(); 681 682 } else { 683 if(!writeResumedForCodecFlush) { 684 writeResumedForCodecFlush = true; 685 resumeWrite(); 686 } 687 } 688 } catch (IOException e) { 689 onTransportFailure(e); 690 } 691 } 692 693 protected boolean transportFlush() throws IOException { 694 return true; 695 } 696 697 public void drainInbound() { 698 if (!getServiceState().isStarted() || readSource.isSuspended()) { 699 return; 700 } 701 try { 702 long initial = codec.getReadCounter(); 703 // Only process upto 2 x the read buffer worth of data at a time so we can give 704 // other connections a chance to process their requests. 705 while( codec.getReadCounter()-initial < codec.getReadBufferSize()<<2 ) { 706 Object command = codec.read(); 707 if ( command!=null ) { 708 try { 709 listener.onTransportCommand(command); 710 } catch (Throwable e) { 711 e.printStackTrace(); 712 onTransportFailure(new IOException("Transport listener failure.")); 713 } 714 715 // the transport may be suspended after processing a command. 716 if (getServiceState() == STOPPED || readSource.isSuspended()) { 717 return; 718 } 719 } else { 720 return; 721 } 722 } 723 yieldSource.merge(1); 724 } catch (IOException e) { 725 onTransportFailure(e); 726 } 727 } 728 729 public SocketAddress getLocalAddress() { 730 return localAddress; 731 } 732 733 public SocketAddress getRemoteAddress() { 734 return remoteAddress; 735 } 736 737 private boolean assertConnected() { 738 try { 739 if ( !isConnected() ) { 740 throw new IOException("Not connected."); 741 } 742 return true; 743 } catch (IOException e) { 744 onTransportFailure(e); 745 } 746 return false; 747 } 748 749 public void suspendRead() { 750 if( isConnected() && readSource!=null ) { 751 readSource.suspend(); 752 } 753 } 754 755 756 public void resumeRead() { 757 if( isConnected() && readSource!=null ) { 758 if( rateLimitingChannel!=null ) { 759 rateLimitingChannel.resumeRead(); 760 } else { 761 _resumeRead(); 762 } 763 } 764 } 765 766 private void _resumeRead() { 767 readSource.resume(); 768 dispatchQueue.execute(new Task(){ 769 public void run() { 770 drainInbound(); 771 } 772 }); 773 } 774 775 protected void suspendWrite() { 776 if( isConnected() && writeSource!=null ) { 777 writeSource.suspend(); 778 } 779 } 780 781 protected void resumeWrite() { 782 if( isConnected() && writeSource!=null ) { 783 writeSource.resume(); 784 } 785 } 786 787 public TransportListener getTransportListener() { 788 return listener; 789 } 790 791 public void setTransportListener(TransportListener transportListener) { 792 this.listener = transportListener; 793 } 794 795 public ProtocolCodec getProtocolCodec() { 796 return codec; 797 } 798 799 public void setProtocolCodec(ProtocolCodec protocolCodec) throws Exception { 800 this.codec = protocolCodec; 801 if( channel!=null && codec!=null ) { 802 initializeCodec(); 803 } 804 } 805 806 public boolean isConnected() { 807 return socketState.is(CONNECTED.class); 808 } 809 810 public boolean isClosed() { 811 return getServiceState() == STOPPED; 812 } 813 814 public boolean isUseLocalHost() { 815 return useLocalHost; 816 } 817 818 /** 819 * Sets whether 'localhost' or the actual local host name should be used to 820 * make local connections. On some operating systems such as Macs its not 821 * possible to connect as the local host name so localhost is better. 822 */ 823 public void setUseLocalHost(boolean useLocalHost) { 824 this.useLocalHost = useLocalHost; 825 } 826 827 private void trace(String message) { 828 // TODO: 829 } 830 831 public SocketChannel getSocketChannel() { 832 return channel; 833 } 834 835 public ReadableByteChannel getReadChannel() { 836 initRateLimitingChannel(); 837 if(rateLimitingChannel!=null) { 838 return rateLimitingChannel; 839 } else { 840 return channel; 841 } 842 } 843 844 public WritableByteChannel getWriteChannel() { 845 initRateLimitingChannel(); 846 if(rateLimitingChannel!=null) { 847 return rateLimitingChannel; 848 } else { 849 return channel; 850 } 851 } 852 853 public int getMaxReadRate() { 854 return maxReadRate; 855 } 856 857 public void setMaxReadRate(int maxReadRate) { 858 this.maxReadRate = maxReadRate; 859 } 860 861 public int getMaxWriteRate() { 862 return maxWriteRate; 863 } 864 865 public void setMaxWriteRate(int maxWriteRate) { 866 this.maxWriteRate = maxWriteRate; 867 } 868 869 public int getTrafficClass() { 870 return trafficClass; 871 } 872 873 public void setTrafficClass(int trafficClass) { 874 this.trafficClass = trafficClass; 875 } 876 877 public int getReceiveBufferSize() { 878 return receiveBufferSize; 879 } 880 881 public void setReceiveBufferSize(int receiveBufferSize) { 882 this.receiveBufferSize = receiveBufferSize; 883 if( channel!=null ) { 884 try { 885 channel.socket().setReceiveBufferSize(receiveBufferSize); 886 } catch (SocketException ignore) { 887 } 888 } 889 } 890 891 public int getSendBufferSize() { 892 return sendBufferSize; 893 } 894 895 public void setSendBufferSize(int sendBufferSize) { 896 this.sendBufferSize = sendBufferSize; 897 if( channel!=null ) { 898 try { 899 channel.socket().setReceiveBufferSize(sendBufferSize); 900 } catch (SocketException ignore) { 901 } 902 } 903 } 904 905 public boolean isKeepAlive() { 906 return keepAlive; 907 } 908 909 public void setKeepAlive(boolean keepAlive) { 910 this.keepAlive = keepAlive; 911 } 912 913 public Executor getBlockingExecutor() { 914 return blockingExecutor; 915 } 916 917 public void setBlockingExecutor(Executor blockingExecutor) { 918 this.blockingExecutor = blockingExecutor; 919 } 920 921 public boolean isCloseOnCancel() { 922 return closeOnCancel; 923 } 924 925 public void setCloseOnCancel(boolean closeOnCancel) { 926 this.closeOnCancel = closeOnCancel; 927 } 928}