001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2016-2018 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.net; 020 021import java.io.IOException; 022import java.lang.management.ManagementFactory; 023import java.lang.ref.WeakReference; 024import java.net.InetSocketAddress; 025import java.net.SocketAddress; 026import java.net.StandardProtocolFamily; 027import java.net.UnixDomainSocketAddress; 028import java.nio.channels.SelectionKey; 029import java.nio.channels.ServerSocketChannel; 030import java.nio.channels.SocketChannel; 031import java.util.ArrayList; 032import java.util.Collections; 033import java.util.Comparator; 034import java.util.HashSet; 035import java.util.IntSummaryStatistics; 036import java.util.List; 037import java.util.Optional; 038import java.util.Set; 039import java.util.SortedMap; 040import java.util.TreeMap; 041import java.util.stream.Collectors; 042import javax.management.InstanceAlreadyExistsException; 043import javax.management.MBeanRegistrationException; 044import javax.management.MBeanServer; 045import javax.management.MalformedObjectNameException; 046import javax.management.NotCompliantMBeanException; 047import javax.management.ObjectName; 048import org.jgrapes.core.Channel; 049import org.jgrapes.core.Components; 050import org.jgrapes.core.Event; 051import org.jgrapes.core.Manager; 052import org.jgrapes.core.Self; 053import org.jgrapes.core.Subchannel; 054import org.jgrapes.core.annotation.Handler; 055import org.jgrapes.core.events.Error; 056import org.jgrapes.core.events.Start; 057import org.jgrapes.core.events.Stop; 058import org.jgrapes.io.NioHandler; 059import org.jgrapes.io.events.Close; 060import org.jgrapes.io.events.Closed; 061import org.jgrapes.io.events.IOError; 062import org.jgrapes.io.events.Input; 063import org.jgrapes.io.events.NioRegistration; 064import org.jgrapes.io.events.NioRegistration.Registration; 065import org.jgrapes.io.events.Opening; 066import org.jgrapes.io.events.Output; 067import org.jgrapes.io.events.Purge; 068import org.jgrapes.io.util.AvailabilityListener; 069import org.jgrapes.io.util.LinkedIOSubchannel; 070import org.jgrapes.io.util.PermitsPool; 071import org.jgrapes.net.events.Accepted; 072import org.jgrapes.net.events.Ready; 073import org.jgrapes.util.events.ConfigurationUpdate; 074 075/** 076 * Provides a socket server. The server binds to the given address. If the 077 * address is {@code null}, address and port are automatically assigned. 078 * The port may be overwritten by a configuration event 079 * (see {@link #onConfigurationUpdate(ConfigurationUpdate)}). 080 * 081 * For each established connection, the server creates a new 082 * {@link LinkedIOSubchannel}. The servers basic operation is to 083 * fire {@link Input} (and {@link Closed}) events on the 084 * appropriate subchannel in response to data received from the 085 * network and to handle {@link Output} (and {@link Close}) events 086 * on the subchannel and forward the information to the network 087 * connection. 088 * 089 * The server supports limiting the number of concurrent connections 090 * with a {@link PermitsPool}. If such a pool is set as connection 091 * limiter (see {@link #setConnectionLimiter(PermitsPool)}), a 092 * permit is acquired for each new connection attempt. If no more 093 * permits are available, the server sends a {@link Purge} event on 094 * each channel that is purgeable for at least the time span 095 * set with {@link #setMinimalPurgeableTime(long)}. Purgeability 096 * is derived from the end of record flag of {@link Output} events 097 * (see {@link #onOutput(Output, SocketChannelImpl)}. When using this feature, 098 * make sure that connections are either short lived or the application 099 * level components support the {@link Purge} event. Else, it may become 100 * impossible to establish new connections. 101 */ 102@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.ExcessivePublicCount", 103 "PMD.NcssCount", "PMD.EmptyCatchBlock", "PMD.AvoidDuplicateLiterals", 104 "PMD.ExcessiveClassLength", "PMD.CouplingBetweenObjects" }) 105public class SocketServer extends SocketConnectionManager 106 implements NioHandler { 107 108 private SocketAddress serverAddress; 109 private ServerSocketChannel serverSocketChannel; 110 private boolean closing; 111 private int backlog; 112 private PermitsPool connLimiter; 113 private Registration registration; 114 @SuppressWarnings("PMD.SingularField") 115 private Purger purger; 116 private long minimumPurgeableTime; 117 118 /** 119 * The purger thread. 120 */ 121 private class Purger extends Thread implements AvailabilityListener { 122 123 private boolean permitsAvailable = true; 124 125 /** 126 * Instantiates a new purger. 127 */ 128 public Purger() { 129 setName(Components.simpleObjectName(this)); 130 setDaemon(true); 131 } 132 133 @Override 134 public void availabilityChanged(PermitsPool pool, boolean available) { 135 if (registration == null) { 136 return; 137 } 138 synchronized (this) { 139 permitsAvailable = available; 140 registration.updateInterested( 141 permitsAvailable ? SelectionKey.OP_ACCEPT : 0); 142 if (!permitsAvailable) { 143 this.notifyAll(); 144 } 145 } 146 } 147 148 @Override 149 @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", 150 "PMD.DataflowAnomalyAnalysis", "PMD.CognitiveComplexity" }) 151 public void run() { 152 if (connLimiter == null) { 153 return; 154 } 155 try { 156 connLimiter.addListener(this); 157 while (serverSocketChannel.isOpen()) { 158 synchronized (this) { 159 while (permitsAvailable) { 160 wait(); 161 } 162 } 163 // Copy to avoid ConcurrentModificationException 164 List<SocketChannelImpl> candidates; 165 synchronized (channels) { 166 candidates = new ArrayList<>(channels); 167 } 168 long purgeableSince 169 = System.currentTimeMillis() - minimumPurgeableTime; 170 candidates = candidates.stream() 171 .filter(channel -> channel.isPurgeable() 172 && channel.purgeableSince() < purgeableSince) 173 .sorted(new Comparator<>() { 174 @Override 175 @SuppressWarnings("PMD.ShortVariable") 176 public int compare(SocketChannelImpl c1, 177 SocketChannelImpl c2) { 178 if (c1.purgeableSince() < c2 179 .purgeableSince()) { 180 return 1; 181 } 182 if (c1.purgeableSince() > c2 183 .purgeableSince()) { 184 return -1; 185 } 186 return 0; 187 } 188 }) 189 .collect(Collectors.toList()); 190 for (SocketChannelImpl channel : candidates) { 191 // Sorting may have taken time... 192 if (!channel.isPurgeable()) { 193 continue; 194 } 195 channel.downPipeline().fire(new Purge(), channel); 196 // Continue only as long as necessary 197 if (permitsAvailable) { 198 break; 199 } 200 } 201 sleep(1000); 202 } 203 } catch (InterruptedException e) { 204 // Fall through 205 } finally { 206 connLimiter.removeListener(this); 207 } 208 } 209 210 } 211 212 /** 213 * Creates a new server, using itself as component channel. 214 */ 215 public SocketServer() { 216 this(Channel.SELF); 217 } 218 219 /** 220 * Creates a new server using the given channel. 221 * 222 * @param componentChannel the component's channel 223 */ 224 public SocketServer(Channel componentChannel) { 225 super(componentChannel); 226 } 227 228 /** 229 * Sets the address to bind to. If none is set, the address and port 230 * are assigned automatically. 231 * 232 * @param serverAddress the address to bind to 233 * @return the socket server for easy chaining 234 */ 235 public SocketServer setServerAddress(SocketAddress serverAddress) { 236 this.serverAddress = serverAddress; 237 return this; 238 } 239 240 @Override 241 public SocketServer setBufferSize(int size) { 242 super.setBufferSize(size); 243 return this; 244 } 245 246 /** 247 * The component can be configured with events that include 248 * a path (see @link {@link ConfigurationUpdate#paths()}) 249 * that matches this components path (see {@link Manager#componentPath()}). 250 * 251 * The following properties are recognized: 252 * 253 * `hostname` 254 * : If given, is used as first parameter for 255 * {@link InetSocketAddress#InetSocketAddress(String, int)}. 256 * 257 * `port` 258 * : If given, is used as parameter for 259 * {@link InetSocketAddress#InetSocketAddress(String, int)} 260 * or {@link InetSocketAddress#InetSocketAddress(int)}, 261 * depending on whether a host name is specified. Defaults to "0". 262 * 263 * `backlog` 264 * : See {@link #setBacklog(int)}. 265 * 266 * `bufferSize` 267 * : See {@link #setBufferSize(int)}. 268 * 269 * `maxConnections` 270 * : Calls {@link #setConnectionLimiter} with a 271 * {@link PermitsPool} of the specified size. 272 * 273 * `minimalPurgeableTime` 274 * : See {@link #setMinimalPurgeableTime(long)}. 275 * 276 * @param event the event 277 */ 278 @Handler 279 @SuppressWarnings("PMD.ConfusingTernary") 280 public void onConfigurationUpdate(ConfigurationUpdate event) { 281 event.values(componentPath()).ifPresent(values -> { 282 String hostname = values.get("hostname"); 283 if (hostname != null) { 284 setServerAddress(new InetSocketAddress(hostname, 285 Integer.parseInt(values.getOrDefault("port", "0")))); 286 } else if (values.containsKey("port")) { 287 setServerAddress(new InetSocketAddress( 288 Integer.parseInt(values.get("port")))); 289 } 290 Optional.ofNullable(values.get("backlog")).ifPresent( 291 value -> setBacklog(Integer.parseInt(value))); 292 Optional.ofNullable(values.get("bufferSize")).ifPresent( 293 value -> setBufferSize(Integer.parseInt(value))); 294 Optional.ofNullable(values.get("maxConnections")) 295 .map(Integer::parseInt).map(PermitsPool::new) 296 .ifPresent(this::setConnectionLimiter); 297 Optional.ofNullable(values.get("minimalPurgeableTime")) 298 .map(Long::parseLong).ifPresent(this::setMinimalPurgeableTime); 299 }); 300 } 301 302 /** 303 * Returns the server address. Before starting, the address is the 304 * address set with {@link #setServerAddress(InetSocketAddress)}. After 305 * starting the address is obtained from the created socket. 306 * 307 * @return the serverAddress 308 */ 309 public SocketAddress serverAddress() { 310 try { 311 return serverSocketChannel == null ? serverAddress 312 : serverSocketChannel.getLocalAddress(); 313 } catch (IOException e) { 314 return serverAddress; 315 } 316 } 317 318 /** 319 * Sets the backlog size. 320 * 321 * @param backlog the backlog to set 322 * @return the socket server for easy chaining 323 */ 324 public SocketServer setBacklog(int backlog) { 325 this.backlog = backlog; 326 return this; 327 } 328 329 /** 330 * Return the configured backlog size. 331 * 332 * @return the backlog 333 */ 334 public int backlog() { 335 return backlog; 336 } 337 338 /** 339 * Sets a permit "pool". A new connection is created only if a permit 340 * can be obtained from the pool. 341 * 342 * A connection limiter must be set before starting the component. 343 * 344 * @param connectionLimiter the connection pool to set 345 * @return the socket server for easy chaining 346 */ 347 public SocketServer setConnectionLimiter(PermitsPool connectionLimiter) { 348 this.connLimiter = connectionLimiter; 349 return this; 350 } 351 352 /** 353 * Returns the connection limiter. 354 * 355 * @return the connection Limiter 356 */ 357 public PermitsPool getConnectionLimiter() { 358 return connLimiter; 359 } 360 361 /** 362 * Sets a minimal time that a connection must be purgeable (idle) 363 * before it may be purged. 364 * 365 * @param millis the millis 366 * @return the socket server 367 */ 368 public SocketServer setMinimalPurgeableTime(long millis) { 369 this.minimumPurgeableTime = millis; 370 return this; 371 } 372 373 /** 374 * Gets the minimal purgeable time. 375 * 376 * @return the minimal purgeable time 377 */ 378 public long getMinimalPurgeableTime() { 379 return minimumPurgeableTime; 380 } 381 382 /** 383 * Starts the server. 384 * 385 * @param event the start event 386 * @throws IOException if an I/O exception occurred 387 */ 388 @Handler 389 public void onStart(Start event) throws IOException { 390 closing = false; 391 if (serverAddress instanceof UnixDomainSocketAddress) { 392 serverSocketChannel 393 = ServerSocketChannel.open(StandardProtocolFamily.UNIX); 394 } else { 395 serverSocketChannel = ServerSocketChannel.open(); 396 } 397 serverSocketChannel.bind(serverAddress, backlog); 398 MBeanView.addServer(this); 399 fire(new NioRegistration(this, serverSocketChannel, 400 SelectionKey.OP_ACCEPT, this), Channel.BROADCAST); 401 } 402 403 /** 404 * Handles the successful channel registration. 405 * 406 * @param event the event 407 * @throws InterruptedException the interrupted exception 408 * @throws IOException Signals that an I/O exception has occurred. 409 */ 410 @Handler(channels = Self.class) 411 public void onRegistered(NioRegistration.Completed event) 412 throws InterruptedException, IOException { 413 NioHandler handler = event.event().handler(); 414 if (handler == this) { 415 if (event.event().get() == null) { 416 fire(new Error(event, 417 "Registration failed, no NioDispatcher?")); 418 return; 419 } 420 registration = event.event().get(); 421 purger = new Purger(); 422 purger.start(); 423 fire(new Ready(serverSocketChannel.getLocalAddress())); 424 return; 425 } 426 if (handler instanceof SocketChannelImpl channel) { 427 var accepted = new Accepted(channel.nioChannel().getLocalAddress(), 428 channel.nioChannel().getRemoteAddress(), false, 429 Collections.emptyList()); 430 var registration = event.event().get(); 431 // (1) Opening, (2) Accepted, (3) process input 432 channel.downPipeline().fire(Event.onCompletion(new Opening<Void>(), 433 e -> { 434 channel.downPipeline().fire(accepted, channel); 435 channel.registrationComplete(registration); 436 }), channel); 437 } 438 } 439 440 /* 441 * (non-Javadoc) 442 * 443 * @see org.jgrapes.io.NioSelectable#handleOps(int) 444 */ 445 @Override 446 public void handleOps(int ops) { 447 if ((ops & SelectionKey.OP_ACCEPT) == 0 || closing) { 448 return; 449 } 450 synchronized (channels) { 451 if (connLimiter != null && !connLimiter.tryAcquire()) { 452 return; 453 } 454 try { 455 @SuppressWarnings("PMD.CloseResource") 456 SocketChannel socketChannel = serverSocketChannel.accept(); 457 if (socketChannel == null) { 458 // "False alarm" 459 if (connLimiter != null) { 460 connLimiter.release(); 461 } 462 return; 463 } 464 channels.add(new SocketChannelImpl(null, socketChannel)); 465 } catch (IOException e) { 466 fire(new IOError(null, e)); 467 } 468 } 469 } 470 471 @Override 472 protected boolean removeChannel(SocketChannelImpl channel) { 473 synchronized (channels) { 474 if (!channels.remove(channel)) { 475 // Closed already 476 return false; 477 } 478 // In case the server is shutting down 479 channels.notifyAll(); 480 } 481 if (connLimiter != null) { 482 connLimiter.release(); 483 } 484 return true; 485 } 486 487 /** 488 * Shuts down the server or one of the connections to the server. 489 * 490 * @param event the event 491 * @throws IOException if an I/O exception occurred 492 * @throws InterruptedException if the execution was interrupted 493 */ 494 @Handler 495 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 496 public void onClose(Close event) throws IOException, InterruptedException { 497 boolean closeServer = false; 498 for (Channel channel : event.channels()) { 499 if (channels.contains(channel)) { 500 ((SocketChannelImpl) channel).close(); 501 continue; 502 } 503 if (channel instanceof Subchannel) { 504 // Some subchannel that we're not interested in. 505 continue; 506 } 507 // Close event on "main" channel 508 closeServer = true; 509 } 510 if (!closeServer) { 511 // Only connection(s) were to be closed. 512 return; 513 } 514 if (!serverSocketChannel.isOpen()) { 515 // Closed already 516 fire(new Closed<Void>()); 517 return; 518 } 519 synchronized (channels) { 520 closing = true; 521 // Copy to avoid concurrent modification exception 522 Set<SocketChannelImpl> conns = new HashSet<>(channels); 523 for (SocketChannelImpl conn : conns) { 524 conn.close(); 525 } 526 while (!channels.isEmpty()) { 527 channels.wait(); 528 } 529 } 530 serverSocketChannel.close(); 531 purger.interrupt(); 532 closing = false; 533 fire(new Closed<Void>()); 534 } 535 536 /** 537 * Shuts down the server by firing a {@link Close} using the 538 * server as channel. Note that this automatically results 539 * in closing all open connections by the runtime system 540 * and thus in {@link Closed} events on all subchannels. 541 * 542 * @param event the event 543 * @throws InterruptedException 544 */ 545 @Handler(priority = -1000) 546 public void onStop(Stop event) throws InterruptedException { 547 if (closing || !serverSocketChannel.isOpen()) { 548 return; 549 } 550 newEventPipeline().fire(new Close(), this).get(); 551 } 552 553 /** 554 * The Interface of the SocketServer MXBean. 555 */ 556 public interface SocketServerMXBean { 557 558 /** 559 * The Class ChannelInfo. 560 */ 561 class ChannelInfo { 562 563 private final SocketChannelImpl channel; 564 565 /** 566 * Instantiates a new channel info. 567 * 568 * @param channel the channel 569 */ 570 public ChannelInfo(SocketChannelImpl channel) { 571 this.channel = channel; 572 } 573 574 /** 575 * Checks if is purgeable. 576 * 577 * @return true, if is purgeable 578 */ 579 public boolean isPurgeable() { 580 return channel.isPurgeable(); 581 } 582 583 /** 584 * Gets the downstream pool. 585 * 586 * @return the downstream pool 587 */ 588 public String getDownstreamPool() { 589 return channel.readBuffers().name(); 590 } 591 592 /** 593 * Gets the upstream pool. 594 * 595 * @return the upstream pool 596 */ 597 public String getUpstreamPool() { 598 return channel.byteBufferPool().name(); 599 } 600 } 601 602 /** 603 * Gets the component path. 604 * 605 * @return the component path 606 */ 607 String getComponentPath(); 608 609 /** 610 * Gets the channel count. 611 * 612 * @return the channel count 613 */ 614 int getChannelCount(); 615 616 /** 617 * Gets the channels. 618 * 619 * @return the channels 620 */ 621 SortedMap<String, ChannelInfo> getChannels(); 622 623 } 624 625 /** 626 * The Class SocketServerInfo. 627 */ 628 public static class SocketServerInfo implements SocketServerMXBean { 629 630 private static MBeanServer mbs 631 = ManagementFactory.getPlatformMBeanServer(); 632 633 private ObjectName mbeanName; 634 private final WeakReference<SocketServer> serverRef; 635 636 /** 637 * Instantiates a new socket server info. 638 * 639 * @param server the server 640 */ 641 @SuppressWarnings({ "PMD.EmptyCatchBlock", 642 "PMD.AvoidCatchingGenericException", 643 "PMD.ConstructorCallsOverridableMethod" }) 644 public SocketServerInfo(SocketServer server) { 645 serverRef = new WeakReference<>(server); 646 try { 647 String endPoint = ""; 648 if (server.serverAddress instanceof InetSocketAddress addr) { 649 endPoint = " (" + addr.getHostName() + ":" + addr.getPort() 650 + ")"; 651 } else if (server.serverAddress instanceof UnixDomainSocketAddress addr) { 652 endPoint = " (" + addr.getPath() + ")"; 653 } 654 mbeanName = new ObjectName("org.jgrapes.io:type=" 655 + SocketServer.class.getSimpleName() + ",name=" 656 + ObjectName 657 .quote(Components.objectName(server) + endPoint)); 658 } catch (MalformedObjectNameException e) { 659 // Should not happen 660 } 661 try { 662 mbs.unregisterMBean(mbeanName); 663 } catch (Exception e) { 664 // Just in case, should not work 665 } 666 try { 667 mbs.registerMBean(this, mbeanName); 668 } catch (InstanceAlreadyExistsException | MBeanRegistrationException 669 | NotCompliantMBeanException e) { 670 // Have to live with that 671 } 672 } 673 674 /** 675 * Server. 676 * 677 * @return the optional 678 */ 679 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 680 "PMD.EmptyCatchBlock" }) 681 public Optional<SocketServer> server() { 682 SocketServer server = serverRef.get(); 683 if (server == null) { 684 try { 685 mbs.unregisterMBean(mbeanName); 686 } catch (Exception e) { 687 // Should work. 688 } 689 } 690 return Optional.ofNullable(server); 691 } 692 693 @Override 694 public String getComponentPath() { 695 return server().map(mgr -> mgr.componentPath()).orElse("<removed>"); 696 } 697 698 @Override 699 public int getChannelCount() { 700 return server().map(server -> server.channels.size()).orElse(0); 701 } 702 703 @Override 704 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 705 public SortedMap<String, ChannelInfo> getChannels() { 706 return server().map(server -> { 707 SortedMap<String, ChannelInfo> result = new TreeMap<>(); 708 for (SocketChannelImpl channel : server.channels) { 709 result.put(channel.nioChannel().socket() 710 .getRemoteSocketAddress().toString(), 711 new ChannelInfo(channel)); 712 } 713 return result; 714 }).orElse(Collections.emptySortedMap()); 715 } 716 } 717 718 /** 719 * An MBean interface for getting information about the socket servers 720 * and established connections. 721 */ 722 public interface SocketServerSummaryMXBean { 723 724 /** 725 * Gets the connections per server statistics. 726 * 727 * @return the connections per server statistics 728 */ 729 IntSummaryStatistics getConnectionsPerServerStatistics(); 730 731 /** 732 * Gets the servers. 733 * 734 * @return the servers 735 */ 736 Set<SocketServerMXBean> getServers(); 737 } 738 739 /** 740 * The MBeanView. 741 */ 742 private static final class MBeanView implements SocketServerSummaryMXBean { 743 private static Set<SocketServerInfo> serverInfos = new HashSet<>(); 744 745 /** 746 * Adds the server to the reported servers. 747 * 748 * @param server the server 749 */ 750 public static void addServer(SocketServer server) { 751 synchronized (serverInfos) { 752 serverInfos.add(new SocketServerInfo(server)); 753 } 754 } 755 756 /** 757 * Returns the infos. 758 * 759 * @return the sets the 760 */ 761 private Set<SocketServerInfo> infos() { 762 Set<SocketServerInfo> expired = new HashSet<>(); 763 synchronized (serverInfos) { 764 for (SocketServerInfo serverInfo : serverInfos) { 765 if (!serverInfo.server().isPresent()) { 766 expired.add(serverInfo); 767 } 768 } 769 serverInfos.removeAll(expired); 770 } 771 return serverInfos; 772 } 773 774 @SuppressWarnings("unchecked") 775 @Override 776 public Set<SocketServerMXBean> getServers() { 777 return (Set<SocketServerMXBean>) (Object) infos(); 778 } 779 780 @Override 781 public IntSummaryStatistics getConnectionsPerServerStatistics() { 782 return infos().stream().map(info -> info.server().get()) 783 .filter(ref -> ref != null).collect( 784 Collectors.summarizingInt(srv -> srv.channels.size())); 785 } 786 } 787 788 static { 789 try { 790 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 791 ObjectName mxbeanName = new ObjectName("org.jgrapes.io:type=" 792 + SocketServer.class.getSimpleName() + "s"); 793 mbs.registerMBean(new MBeanView(), mxbeanName); 794 } catch (MalformedObjectNameException | InstanceAlreadyExistsException 795 | MBeanRegistrationException | NotCompliantMBeanException e) { 796 // Does not happen 797 } 798 } 799 800}